+page.svelte 23.2 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

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
163
164
165
166
	const scrollToBottom = () => {
		const element = document.getElementById('messages-container');
		element.scrollTop = element.scrollHeight;
	};

167
168
169
170
	//////////////////////////
	// Ollama functions
	//////////////////////////

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

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

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

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

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

261
262
			let relevantContexts = await Promise.all(
				docs.map(async (doc) => {
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
					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;
							}
						);
					}
278
279
280
				})
			);
			relevantContexts = relevantContexts.filter((context) => context);
Timothy J. Baek's avatar
Timothy J. Baek committed
281

282
283
284
			const contextString = relevantContexts.reduce((a, context, i, arr) => {
				return `${a}${context.documents.join(' ')}\n`;
			}, '');
Timothy J. Baek's avatar
Timothy J. Baek committed
285

286
			console.log(contextString);
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
287

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

298
		await Promise.all(
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
323
324
325
			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
326

Timothy J. Baek's avatar
Timothy J. Baek committed
327
328
329
330
331
					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
332
				} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
333
					toast.error(`Model ${modelId} not found`);
334
335
336
337
				}
			})
		);

Timothy J. Baek's avatar
Timothy J. Baek committed
338
		await chats.set(await getChatList(localStorage.token));
339
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
340

Timothy J. Baek's avatar
Timothy J. Baek committed
341
	const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
342
		model = model.id;
Timothy J. Baek's avatar
Timothy J. Baek committed
343
		const responseMessage = history.messages[responseMessageId];
344

Timothy J. Baek's avatar
Timothy J. Baek committed
345
		// Wait until history/message have been updated
Timothy J. Baek's avatar
Timothy J. Baek committed
346
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
347
348

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
349
		scrollToBottom();
350

351
352
353
354
355
356
357
		const messagesBody = [
			$settings.system
				? {
						role: 'system',
						content: $settings.system
				  }
				: undefined,
Danny Liu's avatar
Danny Liu committed
358
			...messages
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
		]
			.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;
			}
		});

387
		const [res, controller] = await generateChatCompletion(localStorage.token, {
388
			model: model,
389
			messages: messagesBody,
390
391
392
			options: {
				...($settings.options ?? {})
			},
Zohaib Rauf's avatar
Zohaib Rauf committed
393
394
			format: $settings.requestFormat ?? undefined,
			keep_alive: $settings.keepAlive ?? undefined
395
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
396

397
		if (res && res.ok) {
398
399
			console.log('controller', controller);

Rohit Das's avatar
Rohit Das committed
400
401
402
403
404
405
406
407
408
409
			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;
410
411
412

					if (stopResponseFlag) {
						controller.abort('User: Stop Response');
Timothy J. Baek's avatar
Timothy J. Baek committed
413
						await cancelChatCompletion(localStorage.token, currentRequestId);
414
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
415
416

					currentRequestId = null;
417

Rohit Das's avatar
Rohit Das committed
418
419
					break;
				}
420

Rohit Das's avatar
Rohit Das committed
421
422
				try {
					let lines = value.split('\n');
423

Rohit Das's avatar
Rohit Das committed
424
425
426
427
					for (const line of lines) {
						if (line !== '') {
							console.log(line);
							let data = JSON.parse(line);
Timothy J. Baek's avatar
Timothy J. Baek committed
428

Rohit Das's avatar
Rohit Das committed
429
430
431
							if ('detail' in data) {
								throw data;
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
432

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

466
467
468
469
470
471
472
									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
473
												: `${model}`,
474
475
											{
												body: responseMessage.content,
476
												icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
477
478
479
480
481
482
483
											}
										);
									}

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

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

				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
502
					scrollToBottom();
503
				}
504
			}
505

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

531
532
533
534
			responseMessage.error = true;
			responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`;
			responseMessage.done = true;
			messages = messages;
535
536
537
538
		}

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

540
		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
541
			scrollToBottom();
542
543
544
		}

		if (messages.length == 2 && messages.at(1).content !== '') {
Timothy J. Baek's avatar
Timothy J. Baek committed
545
546
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
			await generateChatTitle(_chatId, userPrompt);
547
548
549
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
550
551
	const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
		const responseMessage = history.messages[responseMessageId];
Timothy J. Baek's avatar
Timothy J. Baek committed
552
		scrollToBottom();
553

Timothy J. Baek's avatar
Timothy J. Baek committed
554
555
556
557
558
559
560
		const res = await generateOpenAIChatCompletion(
			localStorage.token,
			{
				model: model.id,
				stream: true,
				messages: [
					$settings.system
Timothy J. Baek's avatar
Timothy J. Baek committed
561
						? {
Timothy J. Baek's avatar
Timothy J. Baek committed
562
563
								role: 'system',
								content: $settings.system
Timothy J. Baek's avatar
Timothy J. Baek committed
564
						  }
Timothy J. Baek's avatar
Timothy J. Baek committed
565
						: undefined,
Danny Liu's avatar
Danny Liu committed
566
					...messages
Timothy J. Baek's avatar
Timothy J. Baek committed
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
				]
					.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,
				max_tokens: $settings?.options?.num_predict ?? undefined
			},
			model.source === 'litellm' ? `${LITELLM_API_BASE_URL}/v1` : `${OPENAI_API_BASE_URL}`
		);
606

Timothy J. Baek's avatar
Timothy J. Baek committed
607
608
609
610
611
		if (res && res.ok) {
			const reader = res.body
				.pipeThrough(new TextDecoderStream())
				.pipeThrough(splitStream('\n'))
				.getReader();
612

Timothy J. Baek's avatar
Timothy J. Baek committed
613
614
615
616
617
618
619
			while (true) {
				const { value, done } = await reader.read();
				if (done || stopResponseFlag || _chatId !== $chatId) {
					responseMessage.done = true;
					messages = messages;
					break;
				}
620

Timothy J. Baek's avatar
Timothy J. Baek committed
621
622
623
624
625
626
627
628
629
				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;
630
							} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
631
632
633
634
635
636
637
638
639
								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;
								}
640
641
642
							}
						}
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
643
644
645
				} catch (error) {
					console.log(error);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
646

Timothy J. Baek's avatar
Timothy J. Baek committed
647
648
649
				if ($settings.notificationEnabled && !document.hasFocus()) {
					const notification = new Notification(`OpenAI ${model}`, {
						body: responseMessage.content,
650
						icon: `${WEBUI_BASE_URL}/static/favicon.png`
Timothy J. Baek's avatar
Timothy J. Baek committed
651
					});
Timothy J. Baek's avatar
Timothy J. Baek committed
652
653
				}

Timothy J. Baek's avatar
Timothy J. Baek committed
654
655
656
				if ($settings.responseAutoCopy) {
					copyToClipboard(responseMessage.content);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
657

Timothy J. Baek's avatar
Timothy J. Baek committed
658
				if ($settings.responseAutoPlayback) {
Timothy J. Baek's avatar
Timothy J. Baek committed
659
					await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
660
661
662
					document.getElementById(`speak-button-${responseMessage.id}`)?.click();
				}

663
				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
664
					scrollToBottom();
665
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
666
			}
667

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

			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
708
			scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
709
710
711
712
713
		}

		if (messages.length == 2) {
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
			await setChatTitle(_chatId, userPrompt);
714
715
716
717
718
719
720
		}
	};
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
721
722
723
724
725
726
	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];
727
728
729
			responseMessage.done = false;
			await tick();

Timothy J. Baek's avatar
Timothy J. Baek committed
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
			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
747
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
748
749
		} else {
			toast.error(`Model ${modelId} not found`);
Timothy J. Baek's avatar
Timothy J. Baek committed
750
751
752
		}
	};

753
	const regenerateResponse = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
754
		console.log('regenerateResponse');
755
756
757
758
759
760
761
		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
762
			await sendPrompt(userPrompt, userMessage.id);
763
764
765
766
		}
	};

	const generateChatTitle = async (_chatId, userPrompt) => {
767
		if ($settings.titleAutoGenerate ?? true) {
768
769
770
771
772
773
774
			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
775
776
777

			if (title) {
				await setChatTitle(_chatId, title);
778
779
780
			}
		} else {
			await setChatTitle(_chatId, `${userPrompt}`);
781
782
783
784
		}
	};

	const setChatTitle = async (_chatId, _title) => {
785
		if (_chatId === $chatId) {
786
787
			title = _title;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
788
789
790

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

793
794
795
796
797
798
799
800
801
	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
802
803

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

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

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

		chat = await updateChatById(localStorage.token, $chatId, {
Timothy J. Baek's avatar
Timothy J. Baek committed
815
			tags: tags
Timothy J. Baek's avatar
Timothy J. Baek committed
816
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
817
818

		_tags.set(await getAllChatTags(localStorage.token));
819
820
	};

821
822
823
824
825
	onMount(async () => {
		if (!($settings.saveChatHistory ?? true)) {
			await goto('/');
		}
	});
826
827
</script>

828
<svelte:head>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
829
830
831
832
833
	<title>
		{title
			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
			: `${$WEBUI_NAME}`}
	</title>
834
835
</svelte:head>

Timothy J. Baek's avatar
Timothy J. Baek committed
836
{#if loaded}
Timothy J. Baek's avatar
Timothy J. Baek committed
837
	<div class="min-h-screen max-h-screen w-full flex flex-col">
Timothy J. Baek's avatar
Timothy J. Baek committed
838
839
840
841
842
843
844
845
		<Navbar
			{title}
			shareEnabled={messages.length > 0}
			initNewChat={async () => {
				if (currentRequestId !== null) {
					await cancelChatCompletion(localStorage.token, currentRequestId);
					currentRequestId = null;
				}
846

Timothy J. Baek's avatar
Timothy J. Baek committed
847
848
849
850
851
852
				goto('/');
			}}
			{tags}
			{addTag}
			{deleteTag}
		/>
Timothy J. Baek's avatar
Timothy J. Baek committed
853
854
855
856
857
858
859
860
		<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
861
862
863
864
865
866
867
868
869
870
871
				<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
872
				<div class=" h-full w-full flex flex-col py-8">
Timothy J. Baek's avatar
Timothy J. Baek committed
873
874
875
876
877
878
879
880
881
882
883
884
885
886
					<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
887
888
			</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
889
890
891
892
893
894
895
896
897
898
			<MessageInput
				bind:files
				bind:prompt
				bind:autoScroll
				suggestionPrompts={selectedModelfile?.suggestionPrompts ??
					$config.default_prompt_suggestions}
				{messages}
				{submitPrompt}
				{stopResponse}
			/>
899
900
		</div>
	</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
901
{/if}