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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
9
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';
20
	import { copyToClipboard, splitStream } from '$lib/utils';
Timothy J. Baek's avatar
Timothy J. Baek committed
21

22
	import { generateChatCompletion, cancelChatCompletion, generateTitle } 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
		getChatList,
		getTagsById,
		updateChatById
	} from '$lib/apis/chats';
32
	import { queryCollection, queryDoc } from '$lib/apis/rag';
33
	import { generateOpenAIChatCompletion } from '$lib/apis/openai';
34
35
36
37

	import MessageInput from '$lib/components/chat/MessageInput.svelte';
	import Messages from '$lib/components/chat/Messages.svelte';
	import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
38
	import Navbar from '$lib/components/layout/Navbar.svelte';
39
	import { RAGTemplate } from '$lib/utils/rag';
40
	import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants';
41
	import { WEBUI_BASE_URL } from '$lib/constants';
42
43
	let stopResponseFlag = false;
	let autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
44
	let processing = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
45

46
47
	let currentRequestId = null;

Timothy J. Baek's avatar
Timothy J. Baek committed
48
	let selectedModels = [''];
Timothy J. Baek's avatar
Timothy J. Baek committed
49

50
51
52
53
54
55
	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;
56

57
58
59
60
61
62
63
64
65
66
67
	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 })
		};
	}, {});

68
	let chat = null;
69
	let tags = [];
70

Timothy J. Baek's avatar
Timothy J. Baek committed
71
	let title = '';
72
	let pageTitle = WEBUI_NAME;
Timothy J. Baek's avatar
Timothy J. Baek committed
73
	let prompt = '';
74
	let files = [];
75
	let messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
76
77
78
79
80
	let history = {
		messages: {},
		currentId: null
	};

81
82
83
84
85
86
87
	$: if (title) {
		const trimmedTitle = title.length > 30 ? `${title.slice(0, 30)}...` : title;
		pageTitle = `${trimmedTitle} | ${$WEBUI_NAME}`;
	} else {
		pageTitle = $WEBUI_NAME;
	}

Timothy J. Baek's avatar
Timothy J. Baek committed
88
89
90
91
92
93
94
95
96
97
	$: 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
98
99
	} else {
		messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
100
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
101

102
	onMount(async () => {
103
		await initNewChat();
104
105
106
107
108
109
	});

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

110
	const initNewChat = async () => {
111
112
113
114
		if (currentRequestId !== null) {
			await cancelChatCompletion(localStorage.token, currentRequestId);
			currentRequestId = null;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
115
		window.history.replaceState(history.state, '', `/`);
116
		await chatId.set('');
Timothy J. Baek's avatar
Timothy J. Baek committed
117

118
		autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
119

120
121
122
123
124
		title = '';
		messages = [];
		history = {
			messages: {},
			currentId: null
Timothy J. Baek's avatar
Timothy J. Baek committed
125
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
126
127
128
129
130
131
132
133
134
135

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

Timothy J. Baek's avatar
Timothy J. Baek committed
137
138
139
140
		selectedModels = selectedModels.map((modelId) =>
			$models.map((m) => m.id).includes(modelId) ? modelId : ''
		);

141
142
143
144
		let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
		settings.set({
			..._settings
		});
145
146
147

		const chatInput = document.getElementById('chat-textarea');
		setTimeout(() => chatInput?.focus(), 0);
Timothy J. Baek's avatar
Timothy J. Baek committed
148
149
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
150
151
152
153
154
	const scrollToBottom = () => {
		const element = document.getElementById('messages-container');
		element.scrollTop = element.scrollHeight;
	};

155
156
157
158
	//////////////////////////
	// Ollama functions
	//////////////////////////

Timothy J. Baek's avatar
Timothy J. Baek committed
159
	const submitPrompt = async (userPrompt, _user = null) => {
160
161
		console.log('submitPrompt', $chatId);

Timothy J. Baek's avatar
Timothy J. Baek committed
162
163
164
165
		selectedModels = selectedModels.map((modelId) =>
			$models.map((m) => m.id).includes(modelId) ? modelId : ''
		);

166
167
168
169
170
		if (selectedModels.includes('')) {
			toast.error('Model not selected');
		} else if (messages.length != 0 && messages.at(-1).done != true) {
			// Response not done
			console.log('wait');
171
172
173
174
175
176
177
178
		} 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.`
			);
179
180
181
182
183
184
185
186
187
188
189
		} 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
190
				user: _user ?? undefined,
191
				content: userPrompt,
Timothy J. Baek's avatar
Timothy J. Baek committed
192
				files: files.length > 0 ? files : undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
193
				timestamp: Math.floor(Date.now() / 1000) // Unix epoch
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
			};

			// 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,
Timothy J. Baek's avatar
Timothy J. Baek committed
221
						tags: [],
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
						timestamp: Date.now()
					});
					await chats.set(await getChatList(localStorage.token));
					await chatId.set(chat.id);
				} else {
					await chatId.set('local');
				}
				await tick();
			}

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

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

241
242
	const sendPrompt = async (prompt, parentId) => {
		const _chatId = JSON.parse(JSON.stringify($chatId));
243

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

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

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

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

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
293
		await Promise.all(
294
295
			selectedModels.map(async (modelId) => {
				const model = $models.filter((m) => m.id === modelId).at(0);
Timothy J. Baek's avatar
Timothy J. Baek committed
296

Timothy J. Baek's avatar
Timothy J. Baek committed
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
				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
321

Timothy J. Baek's avatar
Timothy J. Baek committed
322
323
324
325
326
					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
327
				} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
328
					toast.error(`Model ${modelId} not found`);
Timothy J. Baek's avatar
Timothy J. Baek committed
329
330
331
				}
			})
		);
332

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

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

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

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

346
347
348
349
350
351
352
		const messagesBody = [
			$settings.system
				? {
						role: 'system',
						content: $settings.system
				  }
				: undefined,
353
			...messages.filter((message) => !message.deleted)
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
381
		]
			.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;
			}
		});

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

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

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

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

					currentRequestId = null;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

549
550
551
552
553
554
555
		const res = await generateOpenAIChatCompletion(
			localStorage.token,
			{
				model: model.id,
				stream: true,
				messages: [
					$settings.system
Timothy J. Baek's avatar
Timothy J. Baek committed
556
						? {
557
558
								role: 'system',
								content: $settings.system
Timothy J. Baek's avatar
Timothy J. Baek committed
559
						  }
560
						: undefined,
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
561
					...messages.filter((message) => !message.deleted)
562
563
564
565
566
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
				]
					.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}`
		);
601

Timothy J. Baek's avatar
Timothy J. Baek committed
602
603
604
605
606
		if (res && res.ok) {
			const reader = res.body
				.pipeThrough(new TextDecoderStream())
				.pipeThrough(splitStream('\n'))
				.getReader();
607

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

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
649
650
651
				if ($settings.responseAutoCopy) {
					copyToClipboard(responseMessage.content);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
652

Timothy J. Baek's avatar
Timothy J. Baek committed
653
				if ($settings.responseAutoPlayback) {
Timothy J. Baek's avatar
Timothy J. Baek committed
654
					await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
655
656
657
					document.getElementById(`speak-button-${responseMessage.id}`)?.click();
				}

658
				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
659
					scrollToBottom();
660
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
661
			}
662

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

			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
703
			scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
704
705
706
707
708
		}

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

712
713
714
715
716
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

717
	const regenerateResponse = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
718
		console.log('regenerateResponse');
719
720
721
		if (messages.length != 0 && messages.at(-1).done == true) {
			messages.splice(messages.length - 1, 1);
			messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
722

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

Timothy J. Baek's avatar
Timothy J. Baek committed
726
			await sendPrompt(userPrompt, userMessage.id);
Timothy J. Baek's avatar
Timothy J. Baek committed
727
		}
728
	};
729

Timothy J. Baek's avatar
Timothy J. Baek committed
730
731
732
733
734
735
	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];
736
737
738
			responseMessage.done = false;
			await tick();

Timothy J. Baek's avatar
Timothy J. Baek committed
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
			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
756
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
757
758
		} else {
			toast.error(`Model ${modelId} not found`);
Timothy J. Baek's avatar
Timothy J. Baek committed
759
760
761
		}
	};

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

			if (title) {
				await setChatTitle(_chatId, title);
774
775
776
			}
		} else {
			await setChatTitle(_chatId, `${userPrompt}`);
777
778
779
		}
	};

780
781
782
783
784
785
786
787
788
	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
789
790
791
792

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

		_tags.set(await getAllChatTags(localStorage.token));
795
796
797
798
799
	};

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

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

		_tags.set(await getAllChatTags(localStorage.token));
806
807
	};

808
	const setChatTitle = async (_chatId, _title) => {
809
		if (_chatId === $chatId) {
810
			title = _title;
811
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
812

813
814
815
816
		if ($settings.saveChatHistory ?? true) {
			chat = await updateChatById(localStorage.token, _chatId, { title: _title });
			await chats.set(await getChatList(localStorage.token));
		}
817
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
818
819
</script>

820
821
822
823
824
825
<svelte:head>
	<title>
		{pageTitle}
	</title>
</svelte:head>

Timothy J. Baek's avatar
Timothy J. Baek committed
826
<div class="h-screen max-h-[100dvh] w-full flex flex-col">
Timothy J. Baek's avatar
Timothy J. Baek committed
827
	<Navbar {title} shareEnabled={messages.length > 0} {initNewChat} {tags} {addTag} {deleteTag} />
Timothy J. Baek's avatar
Timothy J. Baek committed
828
829
	<div class="flex flex-col flex-auto">
		<div
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
830
			class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
Timothy J. Baek's avatar
Timothy J. Baek committed
831
832
833
834
835
			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
836
837
838
839
840
			<div
				class="{$settings?.fullScreenMode ?? null
					? 'max-w-full'
					: 'max-w-2xl md:px-0'} mx-auto w-full px-4"
			>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
841
842
843
844
				<ModelSelector
					bind:selectedModels
					disabled={messages.length > 0 && !selectedModels.includes('')}
				/>
Timothy J. Baek's avatar
Timothy J. Baek committed
845
846
			</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
847
			<div class=" h-full w-full flex flex-col py-8">
Timothy J. Baek's avatar
Timothy J. Baek committed
848
849
850
851
852
853
854
855
856
857
858
859
860
861
				<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
862
		</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
863

Timothy J. Baek's avatar
Timothy J. Baek committed
864
865
866
867
868
869
870
871
872
		<MessageInput
			bind:files
			bind:prompt
			bind:autoScroll
			suggestionPrompts={selectedModelfile?.suggestionPrompts ?? $config.default_prompt_suggestions}
			{messages}
			{submitPrompt}
			{stopResponse}
		/>
873
874
	</div>
</div>