+page.svelte 28.1 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
18
		tags as _tags,
		showSidebar
Timothy J. Baek's avatar
Timothy J. Baek committed
19
	} from '$lib/stores';
Brandon Hulston's avatar
Brandon Hulston committed
20
	import { copyToClipboard, splitStream, convertMessagesToHistory } from '$lib/utils';
21

Timothy J. Baek's avatar
Timothy J. Baek committed
22
	import { generateChatCompletion, cancelOllamaRequest } 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
34
35
36
37
	import {
		generateOpenAIChatCompletion,
		generateSearchQuery,
		generateTitle
	} from '$lib/apis/openai';
Timothy J. Baek's avatar
Timothy J. Baek committed
38

39
40
41
	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
42

Timothy J. Baek's avatar
Timothy J. Baek committed
43
44
45
46
47
48
	import {
		LITELLM_API_BASE_URL,
		OPENAI_API_BASE_URL,
		OLLAMA_API_BASE_URL,
		WEBUI_BASE_URL
	} from '$lib/constants';
49
	import { createOpenAITextStream } from '$lib/apis/streaming';
50
	import { runWebSearch } from '$lib/apis/rag';
Timothy J. Baek's avatar
Timothy J. Baek committed
51
	import { queryMemory } from '$lib/apis/memories';
52

Ased Mammad's avatar
Ased Mammad committed
53
54
	const i18n = getContext('i18n');

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

57
58
	let stopResponseFlag = false;
	let autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
59
	let processing = '';
60
	let messagesContainerElement: HTMLDivElement;
Timothy J. Baek's avatar
Timothy J. Baek committed
61
62
	let currentRequestId = null;

63
	// let chatId = $page.params.id;
64
	let showModelSelector = true;
65
	let selectedModels = [''];
Timothy J. Baek's avatar
Timothy J. Baek committed
66
67
	let atSelectedModel = '';

68
69
	let useWebSearch = false;

70
	let selectedModelfile = null;
Timothy J. Baek's avatar
Timothy J. Baek committed
71

72
73
74
75
76
	$: selectedModelfile =
		selectedModels.length === 1 &&
		$modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0]).length > 0
			? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
			: null;
77

Timothy J. Baek's avatar
Timothy J. Baek committed
78
79
80
81
82
83
84
85
86
87
88
	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 })
		};
	}, {});

89
	let chat = null;
90
	let tags = [];
91

92
93
	let title = '';
	let prompt = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
94
	let files = [];
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111

	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
112
113
	} else {
		messages = [];
114
115
116
117
	}

	$: if ($page.params.id) {
		(async () => {
118
119
			if (await loadChat()) {
				await tick();
120
				loaded = true;
121

122
				window.setTimeout(() => scrollToBottom(), 0);
123
124
				const chatInput = document.getElementById('chat-textarea');
				chatInput?.focus();
125
126
127
			} else {
				await goto('/');
			}
128
129
130
131
132
133
134
135
136
		})();
	}

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

	const loadChat = async () => {
		await chatId.set($page.params.id);
137
138
		chat = await getChatById(localStorage.token, $chatId).catch(async (error) => {
			await goto('/');
139
			return null;
140
141
142
		});

		if (chat) {
143
			tags = await getTags();
144
145
146
147
148
149
150
151
			const chatContent = chat.chat;

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
180
181
	const scrollToBottom = async () => {
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
182
183
184
		if (messagesContainerElement) {
			messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
185
186
	};

187
188
189
190
	//////////////////////////
	// Ollama functions
	//////////////////////////

Timothy J. Baek's avatar
Timothy J. Baek committed
191
	const submitPrompt = async (userPrompt, _user = null) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
192
193
194
		console.log('submitPrompt', $chatId);

		if (selectedModels.includes('')) {
Ased Mammad's avatar
Ased Mammad committed
195
			toast.error($i18n.t('Model not selected'));
Timothy J. Baek's avatar
Timothy J. Baek committed
196
197
198
		} 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
199
200
201
202
203
204
205
206
		} 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
207
208
209
210
211
212
213
214
215
216
217
		} 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
218
				user: _user ?? undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
219
				content: userPrompt,
Timothy J. Baek's avatar
Timothy J. Baek committed
220
				files: files.length > 0 ? files : undefined,
221
222
				timestamp: Math.floor(Date.now() / 1000), // Unix epoch
				models: selectedModels
Timothy J. Baek's avatar
Timothy J. Baek committed
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
			};

			// 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,
242
						title: $i18n.t('New Chat'),
Timothy J. Baek's avatar
Timothy J. Baek committed
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
						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();
258
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
259
260
261
262
263
264
265
266
			// Reset chat input textarea
			prompt = '';
			files = [];

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
271
272
273
274
275
276
277
278
279
		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
280
281
282
283
284
285
286
287
				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
288
289
290
291
292

				console.log(userContext);
			}
		}

293
		await Promise.all(
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
			(modelId ? [modelId] : atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(
				async (modelId) => {
					console.log('modelId', 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,
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
309
							userContext: userContext,
310
311
312
313
314
315
316
317
318
319
320
321
322
323
							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
324

325
						if (useWebSearch) {
326
							await runWebSearchForPrompt(model.id, parentId, responseMessageId);
327
						}
328

329
330
331
332
333
334
335
						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 }));
Timothy J. Baek's avatar
Timothy J. Baek committed
336
					}
337
338
				}
			)
339
340
		);

Timothy J. Baek's avatar
Timothy J. Baek committed
341
		await chats.set(await getChatList(localStorage.token));
342
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
343

344
	const runWebSearchForPrompt = async (model: string, parentId: string, responseId: string) => {
345
346
347
		const responseMessage = history.messages[responseId];
		responseMessage.progress = $i18n.t('Generating search query');
		messages = messages;
348
		const searchQuery = await generateChatSearchQuery(model, parentId);
349
350
351
352
353
354
355
356
		if (!searchQuery) {
			toast.warning($i18n.t('No search query generated'));
			responseMessage.progress = undefined;
			messages = messages;
			return;
		}
		responseMessage.progress = $i18n.t("Searching the web for '{{searchQuery}}'", { searchQuery });
		messages = messages;
357
		const searchDocument = await runWebSearch(localStorage.token, searchQuery);
358
		if (searchDocument === undefined) {
359
360
361
362
363
			toast.warning($i18n.t('No search results found'));
			responseMessage.progress = undefined;
			messages = messages;
			return;
		}
364
365
		if (!responseMessage.files) {
			responseMessage.files = [];
366
		}
367
		responseMessage.files.push({
368
			collection_name: searchDocument.collection_name,
369
			name: searchQuery,
370
			type: 'websearch',
371
			upload_status: true,
372
			error: '',
373
			urls: searchDocument.filenames
374
375
376
377
378
		});
		responseMessage.progress = undefined;
		messages = messages;
	};

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

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

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
387
		scrollToBottom();
388

389
		const messagesBody = [
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
390
			$settings.system || (responseMessage?.userContext ?? null)
391
392
				? {
						role: 'system',
Timothy J. Baek's avatar
Timothy J. Baek committed
393
						content:
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
394
							$settings.system + (responseMessage?.userContext ?? null)
Timothy J. Baek's avatar
Timothy J. Baek committed
395
396
								? `\n\nUser Context:\n${responseMessage.userContext.join('\n')}`
								: ''
397
398
				  }
				: undefined,
Danny Liu's avatar
Danny Liu committed
399
			...messages
400
401
		]
			.filter((message) => message)
Timothy J. Baek's avatar
Timothy J. Baek committed
402
403
404
405
406
407
408
409
410
411
412
413
414
			.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
415
				if (imageUrls && imageUrls.length > 0 && message.role === 'user') {
Timothy J. Baek's avatar
Timothy J. Baek committed
416
417
418
419
420
					baseMessage.images = imageUrls;
				}

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

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

438
439
440
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
441
				message.files.filter((item) => ['doc', 'collection', 'websearch'].includes(item.type))
442
443
444
			)
			.flat(1);

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

463
		if (res && res.ok) {
464
465
			console.log('controller', controller);

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

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

					currentRequestId = null;
483

Rohit Das's avatar
Rohit Das committed
484
485
					break;
				}
486

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

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

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

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

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

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

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

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

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

577
			if ($chatId == _chatId) {
Timothy J. Baek's avatar
Timothy J. Baek committed
578
579
580
581
582
583
584
				if ($settings.saveChatHistory ?? true) {
					chat = await updateChatById(localStorage.token, _chatId, {
						messages: messages,
						history: history
					});
					await chats.set(await getChatList(localStorage.token));
				}
585
			}
586
587
588
		} else {
			if (res !== null) {
				const error = await res.json();
589
590
591
				console.log(error);
				if ('detail' in error) {
					toast.error(error.detail);
592
					responseMessage.content = error.detail;
593
594
				} else {
					toast.error(error.error);
595
					responseMessage.content = error.error;
596
				}
597
			} else {
Ased Mammad's avatar
Ased Mammad committed
598
599
600
601
602
603
				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'
				});
604
605
			}

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

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

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

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

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

631
632
633
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
634
				message.files.filter((item) => ['doc', 'collection', 'websearch'].includes(item.type))
635
636
637
638
639
			)
			.flat(1);

		console.log(docs);

Timothy J. Baek's avatar
Timothy J. Baek committed
640
641
		scrollToBottom();

642
643
644
645
646
647
648
		try {
			const [res, controller] = await generateOpenAIChatCompletion(
				localStorage.token,
				{
					model: model.id,
					stream: true,
					messages: [
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
649
						$settings.system || (responseMessage?.userContext ?? null)
Timothy J. Baek's avatar
Timothy J. Baek committed
650
							? {
651
									role: 'system',
Timothy J. Baek's avatar
Timothy J. Baek committed
652
									content:
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
653
										$settings.system + (responseMessage?.userContext ?? null)
Timothy J. Baek's avatar
Timothy J. Baek committed
654
655
											? `\n\nUser Context:\n${responseMessage.userContext.join('\n')}`
											: ''
Timothy J. Baek's avatar
Timothy J. Baek committed
656
							  }
657
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
							: 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
710

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

714
			scrollToBottom();
715

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

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

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

733
734
						break;
					}
735

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

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

748
749
750
751
752
753
					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
754

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

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

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

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

		stopResponseFlag = false;
		await tick();

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
796
797
			const _title = await generateChatTitle(userPrompt);
			await setChatTitle(_chatId, _title);
798
799
		}
	};
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
832
833
834
835
836
837
838
	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}}.`, {
				provider: model.name ?? model.id
			}) +
			'\n' +
			errorMessage;
		responseMessage.done = true;

		messages = messages;
	};

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

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
851
852
853
854
855
			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
856
857
858
		}
	};

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

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

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

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

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

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

923
924
	const generateChatSearchQuery = async (modelId: string, messageId: string) => {
		const model = $models.find((model) => model.id === modelId);
925

926
		const taskModelId =
927
			model?.external ?? false
928
929
				? $settings?.title?.modelExternal ?? modelId
				: $settings?.title?.model ?? modelId;
930
		const taskModel = $models.find((model) => model.id === taskModelId);
931

932
933
934
		const userMessage = history.messages[messageId];
		const userPrompt = userMessage.content;

Jun Siang Cheah's avatar
Jun Siang Cheah committed
935
936
937
		const previousMessages = messages
			.filter((message) => message.role === 'user')
			.map((message) => message.content);
938

939
940
		return await generateSearchQuery(
			localStorage.token,
941
			taskModelId,
942
			previousMessages,
943
			userPrompt,
944
945
			taskModel?.external ?? false
				? taskModel?.source?.toLowerCase() === 'litellm'
946
947
948
949
950
951
					? `${LITELLM_API_BASE_URL}/v1`
					: `${OPENAI_API_BASE_URL}`
				: `${OLLAMA_API_BASE_URL}/v1`
		);
	};

952
	const setChatTitle = async (_chatId, _title) => {
953
		if (_chatId === $chatId) {
954
955
			title = _title;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
956

Timothy J. Baek's avatar
Timothy J. Baek committed
957
958
959
960
		if ($settings.saveChatHistory ?? true) {
			chat = await updateChatById(localStorage.token, _chatId, { title: _title });
			await chats.set(await getChatList(localStorage.token));
		}
961
	};
962

963
964
965
966
967
968
969
970
971
	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
972
973

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

		_tags.set(await getAllChatTags(localStorage.token));
978
979
980
981
982
	};

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

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

		_tags.set(await getAllChatTags(localStorage.token));
989
990
	};

991
992
993
994
995
	onMount(async () => {
		if (!($settings.saveChatHistory ?? true)) {
			await goto('/');
		}
	});
996
997
</script>

998
<svelte:head>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
999
1000
1001
1002
1003
	<title>
		{title
			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
			: `${$WEBUI_NAME}`}
	</title>
1004
1005
</svelte:head>

Timothy J. Baek's avatar
Timothy J. Baek committed
1006
{#if loaded}
Timothy J. Baek's avatar
Timothy J. Baek committed
1007
1008
	<div
		class="min-h-screen max-h-screen {$showSidebar
Timothy J. Baek's avatar
Timothy J. Baek committed
1009
			? 'md:max-w-[calc(100%-260px)]'
Timothy J. Baek's avatar
Timothy J. Baek committed
1010
			: ''} w-full max-w-full flex flex-col"
Timothy J. Baek's avatar
Timothy J. Baek committed
1011
	>
Timothy J. Baek's avatar
Timothy J. Baek committed
1012
1013
		<Navbar
			{title}
Timothy J. Baek's avatar
Timothy J. Baek committed
1014
			{chat}
Timothy J. Baek's avatar
Timothy J. Baek committed
1015
1016
			bind:selectedModels
			bind:showModelSelector
Timothy J. Baek's avatar
Timothy J. Baek committed
1017
1018
1019
			shareEnabled={messages.length > 0}
			initNewChat={async () => {
				if (currentRequestId !== null) {
Timothy J. Baek's avatar
Timothy J. Baek committed
1020
					await cancelOllamaRequest(localStorage.token, currentRequestId);
Timothy J. Baek's avatar
Timothy J. Baek committed
1021
1022
					currentRequestId = null;
				}
1023

Timothy J. Baek's avatar
Timothy J. Baek committed
1024
1025
1026
				goto('/');
			}}
		/>
Timothy J. Baek's avatar
Timothy J. Baek committed
1027
1028
		<div class="flex flex-col flex-auto">
			<div
Timothy J. Baek's avatar
Timothy J. Baek committed
1029
				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
1030
				id="messages-container"
1031
				bind:this={messagesContainerElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
1032
				on:scroll={(e) => {
1033
1034
					autoScroll =
						messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
1035
						messagesContainerElement.clientHeight + 5;
Timothy J. Baek's avatar
Timothy J. Baek committed
1036
1037
				}}
			>
Timothy J. Baek's avatar
Timothy J. Baek committed
1038
				<div class=" h-full w-full flex flex-col py-4">
Timothy J. Baek's avatar
Timothy J. Baek committed
1039
1040
1041
1042
1043
1044
1045
1046
					<Messages
						chatId={$chatId}
						{selectedModels}
						{selectedModelfiles}
						{processing}
						bind:history
						bind:messages
						bind:autoScroll
Timothy J. Baek's avatar
Timothy J. Baek committed
1047
						bind:prompt
Timothy J. Baek's avatar
Timothy J. Baek committed
1048
1049
1050
1051
1052
1053
						bottomPadding={files.length > 0}
						{sendPrompt}
						{continueGeneration}
						{regenerateResponse}
					/>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
1054
			</div>
1055
1056
		</div>
	</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
1057
1058
1059
1060
1061

	<MessageInput
		bind:files
		bind:prompt
		bind:autoScroll
Timothy J. Baek's avatar
Timothy J. Baek committed
1062
		bind:selectedModel={atSelectedModel}
1063
		bind:useWebSearch
Timothy J. Baek's avatar
Timothy J. Baek committed
1064
1065
1066
		{messages}
		{submitPrompt}
		{stopResponse}
1067
		webSearchAvailable={$config.websearch ?? false}
Timothy J. Baek's avatar
Timothy J. Baek committed
1068
	/>
Timothy J. Baek's avatar
Timothy J. Baek committed
1069
{/if}