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

Ased Mammad's avatar
Ased Mammad committed
5
	import { onMount, tick, getContext } from 'svelte';
6
	import { goto } from '$app/navigation';
7
	import { page } from '$app/stores';
Timothy J. Baek's avatar
Timothy J. Baek committed
8
9
10
11
12
13
14
15
	import {
		models,
		modelfiles,
		user,
		settings,
		chats,
		chatId,
		config,
16
		WEBUI_NAME,
Timothy J. Baek's avatar
Timothy J. Baek committed
17
		tags as _tags,
18
19
		showSidebar,
		type Model
Timothy J. Baek's avatar
Timothy J. Baek committed
20
	} from '$lib/stores';
Brandon Hulston's avatar
Brandon Hulston committed
21
	import { copyToClipboard, splitStream, convertMessagesToHistory } from '$lib/utils';
22

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

36
37
38
	import MessageInput from '$lib/components/chat/MessageInput.svelte';
	import Messages from '$lib/components/chat/Messages.svelte';
	import Navbar from '$lib/components/layout/Navbar.svelte';
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
39

Timothy J. Baek's avatar
Timothy J. Baek committed
40
41
42
43
44
45
	import {
		LITELLM_API_BASE_URL,
		OPENAI_API_BASE_URL,
		OLLAMA_API_BASE_URL,
		WEBUI_BASE_URL
	} from '$lib/constants';
46
	import { createOpenAITextStream } from '$lib/apis/streaming';
Timothy J. Baek's avatar
Timothy J. Baek committed
47
	import { queryMemory } from '$lib/apis/memories';
48

Ased Mammad's avatar
Ased Mammad committed
49
50
	const i18n = getContext('i18n');

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

53
54
	let stopResponseFlag = false;
	let autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
55
	let processing = '';
56
	let messagesContainerElement: HTMLDivElement;
Timothy J. Baek's avatar
Timothy J. Baek committed
57
58
	let currentRequestId = null;

59
	// let chatId = $page.params.id;
60
	let showModelSelector = true;
61
	let selectedModels = [''];
62
	let atSelectedModel: Model | undefined;
Timothy J. Baek's avatar
Timothy J. Baek committed
63

64
	let selectedModelfile = null;
Timothy J. Baek's avatar
Timothy J. Baek committed
65

66
67
68
69
70
	$: selectedModelfile =
		selectedModels.length === 1 &&
		$modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0]).length > 0
			? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
			: null;
71

Timothy J. Baek's avatar
Timothy J. Baek committed
72
73
74
75
76
77
78
79
80
81
82
	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 })
		};
	}, {});

83
	let chat = null;
84
	let tags = [];
85

86
87
	let title = '';
	let prompt = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
88
	let files = [];
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

	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
106
107
	} else {
		messages = [];
108
109
110
111
	}

	$: if ($page.params.id) {
		(async () => {
112
113
			if (await loadChat()) {
				await tick();
114
				loaded = true;
115

116
				window.setTimeout(() => scrollToBottom(), 0);
117
118
				const chatInput = document.getElementById('chat-textarea');
				chatInput?.focus();
119
120
121
			} else {
				await goto('/');
			}
122
123
124
125
126
127
128
129
130
		})();
	}

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

	const loadChat = async () => {
		await chatId.set($page.params.id);
131
132
		chat = await getChatById(localStorage.token, $chatId).catch(async (error) => {
			await goto('/');
133
			return null;
134
135
136
		});

		if (chat) {
137
			tags = await getTags();
138
139
140
141
142
143
144
145
			const chatContent = chat.chat;

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

				selectedModels =
					(chatContent?.models ?? undefined) !== undefined
						? chatContent.models
Brandon Hulston's avatar
Brandon Hulston committed
146
						: [chatContent.models ?? ''];
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
				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;
			}
171
172
173
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
174
175
	const scrollToBottom = async () => {
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
176
177
178
		if (messagesContainerElement) {
			messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
179
180
	};

181
182
183
184
	//////////////////////////
	// Ollama functions
	//////////////////////////

Timothy J. Baek's avatar
Timothy J. Baek committed
185
	const submitPrompt = async (userPrompt, _user = null) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
186
187
188
		console.log('submitPrompt', $chatId);

		if (selectedModels.includes('')) {
Ased Mammad's avatar
Ased Mammad committed
189
			toast.error($i18n.t('Model not selected'));
Timothy J. Baek's avatar
Timothy J. Baek committed
190
191
192
		} 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
193
194
195
196
197
198
199
200
		} 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
201
202
203
204
205
206
207
208
209
210
211
		} 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
212
				user: _user ?? undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
213
				content: userPrompt,
Timothy J. Baek's avatar
Timothy J. Baek committed
214
				files: files.length > 0 ? files : undefined,
215
216
				timestamp: Math.floor(Date.now() / 1000), // Unix epoch
				models: selectedModels
Timothy J. Baek's avatar
Timothy J. Baek committed
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,
236
						title: $i18n.t('New Chat'),
Timothy J. Baek's avatar
Timothy J. Baek committed
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
						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();
252
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
253
254
255
256
257
258
259
260
			// Reset chat input textarea
			prompt = '';
			files = [];

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

	const sendPrompt = async (prompt, parentId, modelId = null) => {
263
		const _chatId = JSON.parse(JSON.stringify($chatId));
Timothy J. Baek's avatar
Timothy J. Baek committed
264

Timothy J. Baek's avatar
Timothy J. Baek committed
265
266
267
268
269
270
271
272
273
		let userContext = null;

		if ($settings?.memory ?? false) {
			const res = await queryMemory(localStorage.token, prompt).catch((error) => {
				toast.error(error);
				return null;
			});

			if (res) {
Timothy J. Baek's avatar
Timothy J. Baek committed
274
275
276
277
278
279
280
281
				if (res.documents[0].length > 0) {
					userContext = res.documents.reduce((acc, doc, index) => {
						const createdAtTimestamp = res.metadatas[index][0].created_at;
						const createdAtDate = new Date(createdAtTimestamp * 1000).toISOString().split('T')[0];
						acc.push(`${index + 1}. [${createdAtDate}]. ${doc[0]}`);
						return acc;
					}, []);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
282
283
284
285
286

				console.log(userContext);
			}
		}

287
		await Promise.all(
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
			(modelId
				? [modelId]
				: atSelectedModel !== undefined
				? [atSelectedModel.id]
				: selectedModels
			).map(async (modelId) => {
				console.log('modelId', modelId);
				const model = $models.filter((m) => m.id === modelId).at(0);

				if (model) {
					// If there are image files, check if model is vision capable
					const hasImages = messages.some((message) =>
						message.files?.some((file) => file.type === 'image')
					);
					if (hasImages && !(model.custom_info?.params.vision_capable ?? true)) {
						toast.error(
							$i18n.t('Model {{modelName}} is not vision capable', {
								modelName: model.custom_info?.name ?? model.name ?? model.id
							})
307
						);
308
309
310
311
312
313
314
315
316
						// Create response message
						let responseMessageId = uuidv4();
						let responseMessage = {
							parentId: parentId,
							id: responseMessageId,
							childrenIds: [],
							role: 'assistant',
							content: '',
							model: model.id,
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
317
							userContext: userContext,
318
319
320
321
322
323
324
325
326
327
328
329
330
331
							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
332

333
334
335
336
337
338
339
						if (model?.external) {
							await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
						} else if (model) {
							await sendPromptOllama(model, prompt, responseMessageId, _chatId);
						}
					} else {
						toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
340
					}
341

342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
					// Create response message
					let responseMessageId = uuidv4();
					let responseMessage = {
						parentId: parentId,
						id: responseMessageId,
						childrenIds: [],
						role: 'assistant',
						content: '',
						model: model.id,
						modelName: model.custom_info?.name ?? model.name ?? 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
365
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
366

367
368
369
370
					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
371
					}
372
373
				} else {
					toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
374
				}
375
			})
376
377
		);

Timothy J. Baek's avatar
Timothy J. Baek committed
378
		await chats.set(await getChatList(localStorage.token));
379
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
380

Timothy J. Baek's avatar
Timothy J. Baek committed
381
	const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
382
		model = model.id;
Timothy J. Baek's avatar
Timothy J. Baek committed
383
		const responseMessage = history.messages[responseMessageId];
384

Timothy J. Baek's avatar
Timothy J. Baek committed
385
		// Wait until history/message have been updated
Timothy J. Baek's avatar
Timothy J. Baek committed
386
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
387
388

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
389
		scrollToBottom();
390

391
		const messagesBody = [
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
392
			$settings.system || (responseMessage?.userContext ?? null)
393
394
				? {
						role: 'system',
Timothy J. Baek's avatar
Timothy J. Baek committed
395
						content:
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
396
							$settings.system + (responseMessage?.userContext ?? null)
Timothy J. Baek's avatar
Timothy J. Baek committed
397
398
								? `\n\nUser Context:\n${responseMessage.userContext.join('\n')}`
								: ''
399
400
				  }
				: undefined,
Danny Liu's avatar
Danny Liu committed
401
			...messages
402
403
		]
			.filter((message) => message)
Timothy J. Baek's avatar
Timothy J. Baek committed
404
405
406
407
408
409
410
411
412
413
414
415
416
			.map((message, idx, arr) => {
				// Prepare the base message object
				const baseMessage = {
					role: message.role,
					content: arr.length - 2 !== idx ? message.content : message?.raContent ?? message.content
				};

				// Extract and format image URLs if any exist
				const imageUrls = message.files
					?.filter((file) => file.type === 'image')
					.map((file) => file.url.slice(file.url.indexOf(',') + 1));

				// Add images array only if it contains elements
417
				if (imageUrls && imageUrls.length > 0 && message.role === 'user') {
Timothy J. Baek's avatar
Timothy J. Baek committed
418
419
420
421
422
					baseMessage.images = imageUrls;
				}

				return baseMessage;
			});
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439

		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;
			}
		});

440
441
442
443
444
445
446
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

447
		const [res, controller] = await generateChatCompletion(localStorage.token, {
448
			model: model,
449
			messages: messagesBody,
450
			options: {
451
452
453
454
455
456
457
				...($settings.options ?? {}),
				stop:
					$settings?.options?.stop ?? undefined
						? $settings.options.stop.map((str) =>
								decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
						  )
						: undefined
458
			},
Zohaib Rauf's avatar
Zohaib Rauf committed
459
			format: $settings.requestFormat ?? undefined,
460
			keep_alive: $settings.keepAlive ?? undefined,
461
462
			docs: docs.length > 0 ? docs : undefined,
			citations: docs.length > 0
463
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
464

465
		if (res && res.ok) {
466
467
			console.log('controller', controller);

Rohit Das's avatar
Rohit Das committed
468
469
470
471
472
473
474
475
476
477
			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;
478
479
480

					if (stopResponseFlag) {
						controller.abort('User: Stop Response');
Timothy J. Baek's avatar
Timothy J. Baek committed
481
						await cancelOllamaRequest(localStorage.token, currentRequestId);
482
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
483
484

					currentRequestId = null;
485

Rohit Das's avatar
Rohit Das committed
486
487
					break;
				}
488

Rohit Das's avatar
Rohit Das committed
489
490
				try {
					let lines = value.split('\n');
491

Rohit Das's avatar
Rohit Das committed
492
493
494
495
					for (const line of lines) {
						if (line !== '') {
							console.log(line);
							let data = JSON.parse(line);
Timothy J. Baek's avatar
Timothy J. Baek committed
496

497
498
499
500
501
							if ('citations' in data) {
								responseMessage.citations = data.citations;
								continue;
							}

Rohit Das's avatar
Rohit Das committed
502
503
504
							if ('detail' in data) {
								throw data;
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
505

506
507
							if ('id' in data) {
								console.log(data);
Timothy J. Baek's avatar
Timothy J. Baek committed
508
								currentRequestId = data.id;
509
510
511
512
513
514
515
516
							} 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
517
								} else {
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
									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
537
									messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
538

539
540
541
542
543
544
545
									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
546
												: `${model}`,
547
548
											{
												body: responseMessage.content,
549
												icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
550
551
552
553
554
555
556
											}
										);
									}

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

									if ($settings.responseAutoPlayback) {
Timothy J. Baek's avatar
Timothy J. Baek committed
559
										await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
560
561
										document.getElementById(`speak-button-${responseMessage.id}`)?.click();
									}
Timothy J. Baek's avatar
Timothy J. Baek committed
562
								}
563
564
565
							}
						}
					}
Rohit Das's avatar
Rohit Das committed
566
567
568
569
570
571
				} catch (error) {
					console.log(error);
					if ('detail' in error) {
						toast.error(error.detail);
					}
					break;
572
				}
Rohit Das's avatar
Rohit Das committed
573
574

				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
575
					scrollToBottom();
576
				}
577
			}
578

579
			if ($chatId == _chatId) {
Timothy J. Baek's avatar
Timothy J. Baek committed
580
581
582
583
584
585
586
				if ($settings.saveChatHistory ?? true) {
					chat = await updateChatById(localStorage.token, _chatId, {
						messages: messages,
						history: history
					});
					await chats.set(await getChatList(localStorage.token));
				}
587
			}
588
589
590
		} else {
			if (res !== null) {
				const error = await res.json();
591
592
593
				console.log(error);
				if ('detail' in error) {
					toast.error(error.detail);
594
					responseMessage.content = error.detail;
595
596
				} else {
					toast.error(error.error);
597
					responseMessage.content = error.error;
598
				}
599
			} else {
Ased Mammad's avatar
Ased Mammad committed
600
601
602
603
604
605
				toast.error(
					$i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { provider: 'Ollama' })
				);
				responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
					provider: 'Ollama'
				});
606
607
			}

608
			responseMessage.error = true;
Ased Mammad's avatar
Ased Mammad committed
609
610
611
			responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
				provider: 'Ollama'
			});
612
613
			responseMessage.done = true;
			messages = messages;
614
615
616
617
		}

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

619
		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
620
			scrollToBottom();
621
622
623
		}

		if (messages.length == 2 && messages.at(1).content !== '') {
Timothy J. Baek's avatar
Timothy J. Baek committed
624
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
Timothy J. Baek's avatar
Timothy J. Baek committed
625
626
			const _title = await generateChatTitle(userPrompt);
			await setChatTitle(_chatId, _title);
627
628
629
		}
	};

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

633
634
635
636
637
638
639
640
641
		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
642
643
		scrollToBottom();

644
645
646
647
648
649
650
		try {
			const [res, controller] = await generateOpenAIChatCompletion(
				localStorage.token,
				{
					model: model.id,
					stream: true,
					messages: [
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
651
						$settings.system || (responseMessage?.userContext ?? null)
Timothy J. Baek's avatar
Timothy J. Baek committed
652
							? {
653
									role: 'system',
Timothy J. Baek's avatar
Timothy J. Baek committed
654
									content:
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
655
										$settings.system + (responseMessage?.userContext ?? null)
Timothy J. Baek's avatar
Timothy J. Baek committed
656
657
											? `\n\nUser Context:\n${responseMessage.userContext.join('\n')}`
											: ''
Timothy J. Baek's avatar
Timothy J. Baek committed
658
							  }
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
							: undefined,
						...messages
					]
						.filter((message) => message)
						.map((message, idx, arr) => ({
							role: message.role,
							...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) &&
							message.role === 'user'
								? {
										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
							? $settings.options.stop.map((str) =>
									decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
							  )
							: 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,
					docs: docs.length > 0 ? docs : undefined,
					citations: docs.length > 0
				},
				model?.source?.toLowerCase() === 'litellm'
					? `${LITELLM_API_BASE_URL}/v1`
					: `${OPENAI_API_BASE_URL}`
			);
Timothy J. Baek's avatar
Timothy J. Baek committed
712

713
714
			// Wait until history/message have been updated
			await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
715

716
			scrollToBottom();
717

718
719
			if (res && res.ok && res.body) {
				const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
720

721
722
723
724
725
				for await (const update of textStream) {
					const { value, done, citations, error } = update;
					if (error) {
						await handleOpenAIError(error, null, model, responseMessage);
						break;
726
					}
727
728
729
					if (done || stopResponseFlag || _chatId !== $chatId) {
						responseMessage.done = true;
						messages = messages;
730

731
732
733
						if (stopResponseFlag) {
							controller.abort('User: Stop Response');
						}
734

735
736
						break;
					}
737

738
739
740
741
					if (citations) {
						responseMessage.citations = citations;
						continue;
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
742

743
744
745
746
747
748
					if (responseMessage.content == '' && value == '\n') {
						continue;
					} else {
						responseMessage.content += value;
						messages = messages;
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
749

750
751
752
753
754
755
					if ($settings.notificationEnabled && !document.hasFocus()) {
						const notification = new Notification(`OpenAI ${model}`, {
							body: responseMessage.content,
							icon: `${WEBUI_BASE_URL}/static/favicon.png`
						});
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
756

757
758
759
					if ($settings.responseAutoCopy) {
						copyToClipboard(responseMessage.content);
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
760

761
762
763
764
					if ($settings.responseAutoPlayback) {
						await tick();
						document.getElementById(`speak-button-${responseMessage.id}`)?.click();
					}
765

766
767
768
					if (autoScroll) {
						scrollToBottom();
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
769
				}
770
771
772
773
774
775
776
777

				if ($chatId == _chatId) {
					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
778
					}
779
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
780
			} else {
781
				await handleOpenAIError(null, res, model, responseMessage);
782
			}
783
784
		} catch (error) {
			await handleOpenAIError(error, null, model, responseMessage);
Timothy J. Baek's avatar
Timothy J. Baek committed
785
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
786
		messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
787
788
789
790
791

		stopResponseFlag = false;
		await tick();

		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
792
			scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
793
794
795
796
		}

		if (messages.length == 2) {
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
797

Timothy J. Baek's avatar
Timothy J. Baek committed
798
799
			const _title = await generateChatTitle(userPrompt);
			await setChatTitle(_chatId, _title);
800
801
		}
	};
802

803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
	const handleOpenAIError = async (error, res: Response | null, model, responseMessage) => {
		let errorMessage = '';
		let innerError;

		if (error) {
			innerError = error;
		} else if (res !== null) {
			innerError = await res.json();
		}
		console.error(innerError);
		if ('detail' in innerError) {
			toast.error(innerError.detail);
			errorMessage = innerError.detail;
		} else if ('error' in innerError) {
			if ('message' in innerError.error) {
				toast.error(innerError.error.message);
				errorMessage = innerError.error.message;
			} else {
				toast.error(innerError.error);
				errorMessage = innerError.error;
			}
		} else if ('message' in innerError) {
			toast.error(innerError.message);
			errorMessage = innerError.message;
		}

		responseMessage.error = true;
		responseMessage.content =
			$i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
832
				provider: model.custom_info?.name ?? model.name ?? model.id
833
834
835
836
837
838
839
840
			}) +
			'\n' +
			errorMessage;
		responseMessage.done = true;

		messages = messages;
	};

841
842
843
844
845
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
846
	const regenerateResponse = async (message) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
847
848
		console.log('regenerateResponse');

Timothy J. Baek's avatar
Timothy J. Baek committed
849
850
		if (messages.length != 0) {
			let userMessage = history.messages[message.parentId];
Timothy J. Baek's avatar
Timothy J. Baek committed
851
852
			let userPrompt = userMessage.content;

Timothy J. Baek's avatar
Timothy J. Baek committed
853
854
855
856
857
			if ((userMessage?.models ?? [...selectedModels]).length == 1) {
				await sendPrompt(userPrompt, userMessage.id);
			} else {
				await sendPrompt(userPrompt, userMessage.id, message.model);
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
858
859
860
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
861
862
863
864
865
866
	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];
867
868
869
			responseMessage.done = false;
			await tick();

Timothy J. Baek's avatar
Timothy J. Baek committed
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
			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
887
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
888
		} else {
Ased Mammad's avatar
Ased Mammad committed
889
			toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
Timothy J. Baek's avatar
Timothy J. Baek committed
890
891
892
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
893
894
895
	const generateChatTitle = async (userPrompt) => {
		if ($settings?.title?.auto ?? true) {
			const model = $models.find((model) => model.id === selectedModels[0]);
896

Timothy J. Baek's avatar
Timothy J. Baek committed
897
898
899
900
901
			const titleModelId =
				model?.external ?? false
					? $settings?.title?.modelExternal ?? selectedModels[0]
					: $settings?.title?.model ?? selectedModels[0];
			const titleModel = $models.find((model) => model.id === titleModelId);
902

Timothy J. Baek's avatar
Timothy J. Baek committed
903
			console.log(titleModel);
904
905
			const title = await generateTitle(
				localStorage.token,
Timothy J. Baek's avatar
Timothy J. Baek committed
906
				$settings?.title?.prompt ??
Ased Mammad's avatar
Ased Mammad committed
907
908
909
					$i18n.t(
						"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}}',
Timothy J. Baek's avatar
Timothy J. Baek committed
910
911
912
				titleModelId,
				userPrompt,
				titleModel?.external ?? false
913
					? titleModel?.source?.toLowerCase() === 'litellm'
Timothy J. Baek's avatar
Timothy J. Baek committed
914
915
916
						? `${LITELLM_API_BASE_URL}/v1`
						: `${OPENAI_API_BASE_URL}`
					: `${OLLAMA_API_BASE_URL}/v1`
917
			);
Timothy J. Baek's avatar
Timothy J. Baek committed
918

Timothy J. Baek's avatar
Timothy J. Baek committed
919
			return title;
920
		} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
921
			return `${userPrompt}`;
922
923
924
925
		}
	};

	const setChatTitle = async (_chatId, _title) => {
926
		if (_chatId === $chatId) {
927
928
			title = _title;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
929

Timothy J. Baek's avatar
Timothy J. Baek committed
930
931
932
933
		if ($settings.saveChatHistory ?? true) {
			chat = await updateChatById(localStorage.token, _chatId, { title: _title });
			await chats.set(await getChatList(localStorage.token));
		}
934
	};
935

936
937
938
939
940
941
942
943
944
	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
945
946

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

		_tags.set(await getAllChatTags(localStorage.token));
951
952
953
954
955
	};

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

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

		_tags.set(await getAllChatTags(localStorage.token));
962
963
	};

964
965
966
967
968
	onMount(async () => {
		if (!($settings.saveChatHistory ?? true)) {
			await goto('/');
		}
	});
969
970
</script>

971
<svelte:head>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
972
973
974
975
976
	<title>
		{title
			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
			: `${$WEBUI_NAME}`}
	</title>
977
978
</svelte:head>

Timothy J. Baek's avatar
Timothy J. Baek committed
979
{#if loaded}
Timothy J. Baek's avatar
Timothy J. Baek committed
980
981
	<div
		class="min-h-screen max-h-screen {$showSidebar
Timothy J. Baek's avatar
Timothy J. Baek committed
982
			? 'md:max-w-[calc(100%-260px)]'
Timothy J. Baek's avatar
Timothy J. Baek committed
983
			: ''} w-full max-w-full flex flex-col"
Timothy J. Baek's avatar
Timothy J. Baek committed
984
	>
Timothy J. Baek's avatar
Timothy J. Baek committed
985
986
		<Navbar
			{title}
Timothy J. Baek's avatar
Timothy J. Baek committed
987
			{chat}
Timothy J. Baek's avatar
Timothy J. Baek committed
988
989
			bind:selectedModels
			bind:showModelSelector
Timothy J. Baek's avatar
Timothy J. Baek committed
990
991
992
			shareEnabled={messages.length > 0}
			initNewChat={async () => {
				if (currentRequestId !== null) {
Timothy J. Baek's avatar
Timothy J. Baek committed
993
					await cancelOllamaRequest(localStorage.token, currentRequestId);
Timothy J. Baek's avatar
Timothy J. Baek committed
994
995
					currentRequestId = null;
				}
996

Timothy J. Baek's avatar
Timothy J. Baek committed
997
998
999
				goto('/');
			}}
		/>
Timothy J. Baek's avatar
Timothy J. Baek committed
1000
1001
		<div class="flex flex-col flex-auto">
			<div
Timothy J. Baek's avatar
Timothy J. Baek committed
1002
				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full"
Timothy J. Baek's avatar
Timothy J. Baek committed
1003
				id="messages-container"
1004
				bind:this={messagesContainerElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
1005
				on:scroll={(e) => {
1006
1007
					autoScroll =
						messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
1008
						messagesContainerElement.clientHeight + 5;
Timothy J. Baek's avatar
Timothy J. Baek committed
1009
1010
				}}
			>
Timothy J. Baek's avatar
Timothy J. Baek committed
1011
				<div class=" h-full w-full flex flex-col py-4">
Timothy J. Baek's avatar
Timothy J. Baek committed
1012
1013
1014
1015
1016
1017
1018
1019
					<Messages
						chatId={$chatId}
						{selectedModels}
						{selectedModelfiles}
						{processing}
						bind:history
						bind:messages
						bind:autoScroll
Timothy J. Baek's avatar
Timothy J. Baek committed
1020
						bind:prompt
Timothy J. Baek's avatar
Timothy J. Baek committed
1021
1022
1023
1024
1025
1026
						bottomPadding={files.length > 0}
						{sendPrompt}
						{continueGeneration}
						{regenerateResponse}
					/>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
1027
			</div>
1028
1029
		</div>
	</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
1030
1031
1032
1033
1034

	<MessageInput
		bind:files
		bind:prompt
		bind:autoScroll
Timothy J. Baek's avatar
Timothy J. Baek committed
1035
		bind:selectedModel={atSelectedModel}
1036
		{selectedModels}
Timothy J. Baek's avatar
Timothy J. Baek committed
1037
1038
1039
1040
		{messages}
		{submitPrompt}
		{stopResponse}
	/>
Timothy J. Baek's avatar
Timothy J. Baek committed
1041
{/if}