+page.svelte 22.5 KB
Newer Older
1
2
<script lang="ts">
	import { v4 as uuidv4 } from 'uuid';
Jannik Streidl's avatar
Jannik Streidl committed
3
	import { toast } from 'svelte-sonner';
4
5
6

	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
	import {
		models,
		modelfiles,
		user,
		settings,
		chats,
		chatId,
		config,
17
		WEBUI_NAME,
Timothy J. Baek's avatar
Timothy J. Baek committed
18
19
		tags as _tags
	} from '$lib/stores';
Brandon Hulston's avatar
Brandon Hulston committed
20
	import { copyToClipboard, splitStream, convertMessagesToHistory } from '$lib/utils';
21

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

36
37
38
39
	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
40
	import { RAGTemplate } from '$lib/utils/rag';
Timothy J. Baek's avatar
Timothy J. Baek committed
41
	import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
42
43

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

45
46
	let stopResponseFlag = false;
	let autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
47
	let processing = '';
48
	let messagesContainerElement: HTMLDivElement;
Timothy J. Baek's avatar
Timothy J. Baek committed
49
50
	let currentRequestId = null;

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

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

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

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

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

	$: if ($page.params.id) {
		(async () => {
100
101
			if (await loadChat()) {
				await tick();
102
				loaded = true;
103

104
				window.setTimeout(() => scrollToBottom(), 0);
105
106
				const chatInput = document.getElementById('chat-textarea');
				chatInput?.focus();
107
108
109
			} else {
				await goto('/');
			}
110
111
112
113
114
115
116
117
118
		})();
	}

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

	const loadChat = async () => {
		await chatId.set($page.params.id);
119
120
		chat = await getChatById(localStorage.token, $chatId).catch(async (error) => {
			await goto('/');
121
			return null;
122
123
124
		});

		if (chat) {
125
			tags = await getTags();
126
127
128
129
130
131
132
133
			const chatContent = chat.chat;

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

				selectedModels =
					(chatContent?.models ?? undefined) !== undefined
						? chatContent.models
Brandon Hulston's avatar
Brandon Hulston committed
134
						: [chatContent.models ?? ''];
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
				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;
			}
159
160
161
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
162
	const scrollToBottom = () => {
163
		messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
Timothy J. Baek's avatar
Timothy J. Baek committed
164
165
	};

166
167
168
169
	//////////////////////////
	// Ollama functions
	//////////////////////////

Timothy J. Baek's avatar
Timothy J. Baek committed
170
	const submitPrompt = async (userPrompt, _user = null) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
171
172
173
174
175
176
177
		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
178
179
180
181
182
183
184
185
		} 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
186
187
188
189
190
191
192
193
194
195
196
		} 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
197
				user: _user ?? undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
198
				content: userPrompt,
Timothy J. Baek's avatar
Timothy J. Baek committed
199
				files: files.length > 0 ? files : undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
200
				timestamp: Math.floor(Date.now() / 1000) // Unix epoch
Timothy J. Baek's avatar
Timothy J. Baek committed
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
232
233
234
235
			};

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

			// Send prompt
			await sendPrompt(userPrompt, userMessageId);
		}
	};
245
246
	const sendPrompt = async (prompt, parentId) => {
		const _chatId = JSON.parse(JSON.stringify($chatId));
Timothy J. Baek's avatar
Timothy J. Baek committed
247

248
		await Promise.all(
Timothy J. Baek's avatar
Timothy J. Baek committed
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
			selectedModels.map(async (modelId) => {
				const model = $models.filter((m) => m.id === modelId).at(0);

				if (model) {
					// Create response message
					let responseMessageId = uuidv4();
					let responseMessage = {
						parentId: parentId,
						id: responseMessageId,
						childrenIds: [],
						role: 'assistant',
						content: '',
						model: model.id,
						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
276

Timothy J. Baek's avatar
Timothy J. Baek committed
277
278
279
280
281
					if (model?.external) {
						await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
					} else if (model) {
						await sendPromptOllama(model, prompt, responseMessageId, _chatId);
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
282
				} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
283
					toast.error(`Model ${modelId} not found`);
284
285
286
287
				}
			})
		);

Timothy J. Baek's avatar
Timothy J. Baek committed
288
		await chats.set(await getChatList(localStorage.token));
289
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
290

Timothy J. Baek's avatar
Timothy J. Baek committed
291
	const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
292
		model = model.id;
Timothy J. Baek's avatar
Timothy J. Baek committed
293
		const responseMessage = history.messages[responseMessageId];
294

Timothy J. Baek's avatar
Timothy J. Baek committed
295
		// Wait until history/message have been updated
Timothy J. Baek's avatar
Timothy J. Baek committed
296
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
297
298

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
299
		scrollToBottom();
300

301
302
303
304
305
306
307
		const messagesBody = [
			$settings.system
				? {
						role: 'system',
						content: $settings.system
				  }
				: undefined,
Danny Liu's avatar
Danny Liu committed
308
			...messages
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
		]
			.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;
			}
		});

337
338
339
340
341
342
343
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

344
		const [res, controller] = await generateChatCompletion(localStorage.token, {
345
			model: model,
346
			messages: messagesBody,
347
348
349
			options: {
				...($settings.options ?? {})
			},
Zohaib Rauf's avatar
Zohaib Rauf committed
350
			format: $settings.requestFormat ?? undefined,
351
352
			keep_alive: $settings.keepAlive ?? undefined,
			docs: docs.length > 0 ? docs : undefined
353
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
354

355
		if (res && res.ok) {
356
357
			console.log('controller', controller);

Rohit Das's avatar
Rohit Das committed
358
359
360
361
362
363
364
365
366
367
			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;
368
369
370

					if (stopResponseFlag) {
						controller.abort('User: Stop Response');
Timothy J. Baek's avatar
Timothy J. Baek committed
371
						await cancelChatCompletion(localStorage.token, currentRequestId);
372
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
373
374

					currentRequestId = null;
375

Rohit Das's avatar
Rohit Das committed
376
377
					break;
				}
378

Rohit Das's avatar
Rohit Das committed
379
380
				try {
					let lines = value.split('\n');
381

Rohit Das's avatar
Rohit Das committed
382
383
384
385
					for (const line of lines) {
						if (line !== '') {
							console.log(line);
							let data = JSON.parse(line);
Timothy J. Baek's avatar
Timothy J. Baek committed
386

Rohit Das's avatar
Rohit Das committed
387
388
389
							if ('detail' in data) {
								throw data;
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
390

391
392
							if ('id' in data) {
								console.log(data);
Timothy J. Baek's avatar
Timothy J. Baek committed
393
								currentRequestId = data.id;
394
395
396
397
398
399
400
401
							} 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
402
								} else {
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
									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
422
									messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
423

424
425
426
427
428
429
430
									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
431
												: `${model}`,
432
433
											{
												body: responseMessage.content,
434
												icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
435
436
437
438
439
440
441
											}
										);
									}

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

									if ($settings.responseAutoPlayback) {
Timothy J. Baek's avatar
Timothy J. Baek committed
444
										await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
445
446
										document.getElementById(`speak-button-${responseMessage.id}`)?.click();
									}
Timothy J. Baek's avatar
Timothy J. Baek committed
447
								}
448
449
450
							}
						}
					}
Rohit Das's avatar
Rohit Das committed
451
452
453
454
455
456
				} catch (error) {
					console.log(error);
					if ('detail' in error) {
						toast.error(error.detail);
					}
					break;
457
				}
Rohit Das's avatar
Rohit Das committed
458
459

				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
460
					scrollToBottom();
461
				}
462
			}
463

464
			if ($chatId == _chatId) {
Timothy J. Baek's avatar
Timothy J. Baek committed
465
466
467
468
469
470
471
				if ($settings.saveChatHistory ?? true) {
					chat = await updateChatById(localStorage.token, _chatId, {
						messages: messages,
						history: history
					});
					await chats.set(await getChatList(localStorage.token));
				}
472
			}
473
474
475
		} else {
			if (res !== null) {
				const error = await res.json();
476
477
478
				console.log(error);
				if ('detail' in error) {
					toast.error(error.detail);
479
					responseMessage.content = error.detail;
480
481
				} else {
					toast.error(error.error);
482
					responseMessage.content = error.error;
483
				}
484
485
			} else {
				toast.error(`Uh-oh! There was an issue connecting to Ollama.`);
486
				responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`;
487
488
			}

489
490
491
492
			responseMessage.error = true;
			responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`;
			responseMessage.done = true;
			messages = messages;
493
494
495
496
		}

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

498
		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
499
			scrollToBottom();
500
501
502
		}

		if (messages.length == 2 && messages.at(1).content !== '') {
Timothy J. Baek's avatar
Timothy J. Baek committed
503
504
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
			await generateChatTitle(_chatId, userPrompt);
505
506
507
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
508
509
	const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
		const responseMessage = history.messages[responseMessageId];
Timothy J. Baek's avatar
Timothy J. Baek committed
510
		scrollToBottom();
511

512
513
514
515
516
517
518
519
520
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

		console.log(docs);

Timothy J. Baek's avatar
Timothy J. Baek committed
521
522
523
524
525
526
527
		const res = await generateOpenAIChatCompletion(
			localStorage.token,
			{
				model: model.id,
				stream: true,
				messages: [
					$settings.system
Timothy J. Baek's avatar
Timothy J. Baek committed
528
						? {
Timothy J. Baek's avatar
Timothy J. Baek committed
529
530
								role: 'system',
								content: $settings.system
Timothy J. Baek's avatar
Timothy J. Baek committed
531
						  }
Timothy J. Baek's avatar
Timothy J. Baek committed
532
						: undefined,
Danny Liu's avatar
Danny Liu committed
533
					...messages
Timothy J. Baek's avatar
Timothy J. Baek committed
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
				]
					.filter((message) => message)
					.map((message, idx, arr) => ({
						role: message.role,
						...(message.files?.filter((file) => file.type === 'image').length > 0 ?? false
							? {
									content: [
										{
											type: 'text',
											text:
												arr.length - 1 !== idx
													? message.content
													: message?.raContent ?? message.content
										},
										...message.files
											.filter((file) => file.type === 'image')
											.map((file) => ({
												type: 'image_url',
												image_url: {
													url: file.url
												}
											}))
									]
							  }
							: {
									content:
										arr.length - 1 !== idx ? message.content : message?.raContent ?? message.content
							  })
					})),
				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,
569
570
				max_tokens: $settings?.options?.num_predict ?? undefined,
				docs: docs.length > 0 ? docs : undefined
Timothy J. Baek's avatar
Timothy J. Baek committed
571
572
573
			},
			model.source === 'litellm' ? `${LITELLM_API_BASE_URL}/v1` : `${OPENAI_API_BASE_URL}`
		);
574

Timothy J. Baek's avatar
Timothy J. Baek committed
575
576
577
578
579
		if (res && res.ok) {
			const reader = res.body
				.pipeThrough(new TextDecoderStream())
				.pipeThrough(splitStream('\n'))
				.getReader();
580

Timothy J. Baek's avatar
Timothy J. Baek committed
581
582
583
584
585
586
587
			while (true) {
				const { value, done } = await reader.read();
				if (done || stopResponseFlag || _chatId !== $chatId) {
					responseMessage.done = true;
					messages = messages;
					break;
				}
588

Timothy J. Baek's avatar
Timothy J. Baek committed
589
590
591
592
593
594
595
596
597
				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;
598
							} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
599
600
601
602
603
604
605
606
607
								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;
								}
608
609
610
							}
						}
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
611
612
613
				} catch (error) {
					console.log(error);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
614

Timothy J. Baek's avatar
Timothy J. Baek committed
615
616
617
				if ($settings.notificationEnabled && !document.hasFocus()) {
					const notification = new Notification(`OpenAI ${model}`, {
						body: responseMessage.content,
618
						icon: `${WEBUI_BASE_URL}/static/favicon.png`
Timothy J. Baek's avatar
Timothy J. Baek committed
619
					});
Timothy J. Baek's avatar
Timothy J. Baek committed
620
621
				}

Timothy J. Baek's avatar
Timothy J. Baek committed
622
623
624
				if ($settings.responseAutoCopy) {
					copyToClipboard(responseMessage.content);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
625

Timothy J. Baek's avatar
Timothy J. Baek committed
626
				if ($settings.responseAutoPlayback) {
Timothy J. Baek's avatar
Timothy J. Baek committed
627
					await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
628
629
630
					document.getElementById(`speak-button-${responseMessage.id}`)?.click();
				}

631
				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
632
					scrollToBottom();
633
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
634
			}
635

Timothy J. Baek's avatar
Timothy J. Baek committed
636
			if ($chatId == _chatId) {
Timothy J. Baek's avatar
Timothy J. Baek committed
637
638
639
640
641
642
643
				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
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
			}
		} 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;
					}
660
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
661
662
663
			} else {
				toast.error(`Uh-oh! There was an issue connecting to ${model}.`);
				responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`;
664
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
665
666
667
668
669
670
671
672
673
674
675

			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
676
			scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
677
678
679
680
681
		}

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

685
686
687
688
689
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
690
691
692
693
694
695
	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];
696
697
698
			responseMessage.done = false;
			await tick();

Timothy J. Baek's avatar
Timothy J. Baek committed
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
			const model = $models.filter((m) => m.id === responseMessage.model).at(0);

			if (model) {
				if (model?.external) {
					await sendPromptOpenAI(
						model,
						history.messages[responseMessage.parentId].content,
						responseMessage.id,
						_chatId
					);
				} else
					await sendPromptOllama(
						model,
						history.messages[responseMessage.parentId].content,
						responseMessage.id,
						_chatId
					);
Timothy J. Baek's avatar
Timothy J. Baek committed
716
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
717
718
		} else {
			toast.error(`Model ${modelId} not found`);
Timothy J. Baek's avatar
Timothy J. Baek committed
719
720
721
		}
	};

722
	const regenerateResponse = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
723
		console.log('regenerateResponse');
724
725
726
727
728
729
730
		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
731
			await sendPrompt(userPrompt, userMessage.id);
732
733
734
735
		}
	};

	const generateChatTitle = async (_chatId, userPrompt) => {
736
		if ($settings.titleAutoGenerate ?? true) {
737
738
739
740
741
742
743
			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
744
745
746

			if (title) {
				await setChatTitle(_chatId, title);
747
748
749
			}
		} else {
			await setChatTitle(_chatId, `${userPrompt}`);
750
751
752
753
		}
	};

	const setChatTitle = async (_chatId, _title) => {
754
		if (_chatId === $chatId) {
755
756
			title = _title;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
757
758
759

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

762
763
764
765
766
767
768
769
770
	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
771
772

		chat = await updateChatById(localStorage.token, $chatId, {
Timothy J. Baek's avatar
Timothy J. Baek committed
773
			tags: tags
Timothy J. Baek's avatar
Timothy J. Baek committed
774
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
775
776

		_tags.set(await getAllChatTags(localStorage.token));
777
778
779
780
781
	};

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

		chat = await updateChatById(localStorage.token, $chatId, {
Timothy J. Baek's avatar
Timothy J. Baek committed
784
			tags: tags
Timothy J. Baek's avatar
Timothy J. Baek committed
785
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
786
787

		_tags.set(await getAllChatTags(localStorage.token));
788
789
	};

790
791
792
793
794
	onMount(async () => {
		if (!($settings.saveChatHistory ?? true)) {
			await goto('/');
		}
	});
795
796
</script>

797
<svelte:head>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
798
799
800
801
802
	<title>
		{title
			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
			: `${$WEBUI_NAME}`}
	</title>
803
804
</svelte:head>

Timothy J. Baek's avatar
Timothy J. Baek committed
805
{#if loaded}
Timothy J. Baek's avatar
Timothy J. Baek committed
806
	<div class="min-h-screen max-h-screen w-full flex flex-col">
Timothy J. Baek's avatar
Timothy J. Baek committed
807
808
809
810
811
812
813
814
		<Navbar
			{title}
			shareEnabled={messages.length > 0}
			initNewChat={async () => {
				if (currentRequestId !== null) {
					await cancelChatCompletion(localStorage.token, currentRequestId);
					currentRequestId = null;
				}
815

Timothy J. Baek's avatar
Timothy J. Baek committed
816
817
818
819
820
821
				goto('/');
			}}
			{tags}
			{addTag}
			{deleteTag}
		/>
Timothy J. Baek's avatar
Timothy J. Baek committed
822
823
824
825
		<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"
826
				bind:this={messagesContainerElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
827
				on:scroll={(e) => {
828
829
830
					autoScroll =
						messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
						messagesContainerElement.clientHeight + 50;
Timothy J. Baek's avatar
Timothy J. Baek committed
831
832
				}}
			>
Timothy J. Baek's avatar
Timothy J. Baek committed
833
834
835
836
837
				<div
					class="{$settings?.fullScreenMode ?? null
						? 'max-w-full'
						: 'max-w-2xl md:px-0'} mx-auto w-full px-4"
				>
838
					<ModelSelector bind:selectedModels />
Timothy J. Baek's avatar
Timothy J. Baek committed
839
840
				</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
841
				<div class=" h-full w-full flex flex-col py-8">
Timothy J. Baek's avatar
Timothy J. Baek committed
842
843
844
845
846
847
848
849
850
851
852
853
854
855
					<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
856
857
			</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
858
859
860
861
862
863
864
865
866
867
			<MessageInput
				bind:files
				bind:prompt
				bind:autoScroll
				suggestionPrompts={selectedModelfile?.suggestionPrompts ??
					$config.default_prompt_suggestions}
				{messages}
				{submitPrompt}
				{stopResponse}
			/>
868
869
		</div>
	</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
870
{/if}