+page.svelte 26 KB
Newer Older
Timothy J. Baek's avatar
Timothy J. Baek committed
1
<script lang="ts">
2
	import { v4 as uuidv4 } from 'uuid';
Jannik Streidl's avatar
Jannik Streidl committed
3
	import { toast } from 'svelte-sonner';
Timothy J. Baek's avatar
Timothy J. Baek committed
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

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

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

	import MessageInput from '$lib/components/chat/MessageInput.svelte';
	import Messages from '$lib/components/chat/Messages.svelte';
	import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
39
	import Navbar from '$lib/components/layout/Navbar.svelte';
40
	import { RAGTemplate } from '$lib/utils/rag';
41
	import { LITELLM_API_BASE_URL, OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants';
42
	import { WEBUI_BASE_URL } from '$lib/constants';
43
	import { createOpenAITextStream } from '$lib/apis/streaming';
Ased Mammad's avatar
Ased Mammad committed
44
45
46

	const i18n = getContext('i18n');

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

53
	let showModelSelector = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
54

Timothy J. Baek's avatar
Timothy J. Baek committed
55
	let selectedModels = [''];
Timothy J. Baek's avatar
Timothy J. Baek committed
56
	let atSelectedModel = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
57

58
59
	let useWebSearch = false;

60
61
62
63
64
65
	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;
66

67
68
69
70
71
72
73
74
75
76
77
	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 })
		};
	}, {});

78
	let chat = null;
79
	let tags = [];
80

Timothy J. Baek's avatar
Timothy J. Baek committed
81
	let title = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
82
	let prompt = '';
83
	let files = [];
84
	let messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
	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
100
101
	} else {
		messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
102
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
103

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

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

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

120
		autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
121

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

		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 = [''];
		}
138

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

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
152
153
	const scrollToBottom = async () => {
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
154
155
156
		if (messagesContainerElement) {
			messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
157
158
	};

159
160
161
162
	//////////////////////////
	// Ollama functions
	//////////////////////////

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

Timothy J. Baek's avatar
Timothy J. Baek committed
166
167
168
169
		selectedModels = selectedModels.map((modelId) =>
			$models.map((m) => m.id).includes(modelId) ? modelId : ''
		);

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

			// 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,
219
						title: $i18n.t('New Chat'),
220
221
222
223
224
225
226
						models: selectedModels,
						system: $settings.system ?? undefined,
						options: {
							...($settings.options ?? {})
						},
						messages: messages,
						history: history,
Timothy J. Baek's avatar
Timothy J. Baek committed
227
						tags: [],
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
						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);
		}
	};

247
248
	const sendPrompt = async (prompt, parentId) => {
		const _chatId = JSON.parse(JSON.stringify($chatId));
249

Timothy J. Baek's avatar
Timothy J. Baek committed
250
		await Promise.all(
Timothy J. Baek's avatar
Timothy J. Baek committed
251
252
			(atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(async (modelId) => {
				console.log('modelId', modelId);
253
				const model = $models.filter((m) => m.id === modelId).at(0);
Timothy J. Baek's avatar
Timothy J. Baek committed
254

Timothy J. Baek's avatar
Timothy J. Baek committed
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
				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
279

280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
					if (useWebSearch) {
						// TODO: Toasts are temporary indicators for web search
						toast.info($i18n.t('Generating search query'));
						const searchQuery = await generateChatSearchQuery(prompt);
						if (searchQuery) {
							toast.info($i18n.t('Searching the web for \'{{searchQuery}}\'', { searchQuery }));
							const searchDocUuid = uuidv4();
							const searchDocument = await runWebSearch(localStorage.token, searchQuery, searchDocUuid);
							if (searchDocument) {
								const parentMessage = history.messages[parentId];
								if (!parentMessage.files) {
									parentMessage.files = [];
								}
								parentMessage.files.push({
									collection_name: searchDocument.collection_name,
									name: searchQuery,
									type: 'doc',
									upload_status: true,
									error: ""
								});
								// Find message in messages and update it
								const messageIndex = messages.findIndex((message) => message.id === parentId);
								if (messageIndex !== -1) {
									messages[messageIndex] = parentMessage;
								}
							} else {
								toast.warning($i18n.t('No search results found'));
							}
						} else {
							toast.warning($i18n.t('No search query generated'));
						}
					}

Timothy J. Baek's avatar
Timothy J. Baek committed
313
314
315
316
317
					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
318
				} else {
Ased Mammad's avatar
Ased Mammad committed
319
					toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
Timothy J. Baek's avatar
Timothy J. Baek committed
320
321
322
				}
			})
		);
323

Timothy J. Baek's avatar
Timothy J. Baek committed
324
		await chats.set(await getChatList(localStorage.token));
325
326
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
327
	const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
328
		model = model.id;
Timothy J. Baek's avatar
Timothy J. Baek committed
329
		const responseMessage = history.messages[responseMessageId];
Timothy J. Baek's avatar
Timothy J. Baek committed
330

Timothy J. Baek's avatar
Timothy J. Baek committed
331
		// Wait until history/message have been updated
Timothy J. Baek's avatar
Timothy J. Baek committed
332
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
333
334

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
335
		scrollToBottom();
336

337
338
339
340
341
342
343
		const messagesBody = [
			$settings.system
				? {
						role: 'system',
						content: $settings.system
				  }
				: undefined,
Danny Liu's avatar
Danny Liu committed
344
			...messages
345
346
		]
			.filter((message) => message)
Timothy J. Baek's avatar
Timothy J. Baek committed
347
348
349
350
351
352
353
354
355
356
357
358
359
			.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
360
				if (imageUrls && imageUrls.length > 0 && message.role === 'user') {
Timothy J. Baek's avatar
Timothy J. Baek committed
361
362
363
364
365
					baseMessage.images = imageUrls;
				}

				return baseMessage;
			});
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382

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

Timothy J. Baek's avatar
Timothy J. Baek committed
383
384
385
386
387
388
389
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

390
		const [res, controller] = await generateChatCompletion(localStorage.token, {
391
			model: model,
392
			messages: messagesBody,
393
			options: {
394
395
396
397
398
399
400
				...($settings.options ?? {}),
				stop:
					$settings?.options?.stop ?? undefined
						? $settings.options.stop.map((str) =>
								decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
						  )
						: undefined
401
			},
Zohaib Rauf's avatar
Zohaib Rauf committed
402
			format: $settings.requestFormat ?? undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
403
			keep_alive: $settings.keepAlive ?? undefined,
404
405
			docs: docs.length > 0 ? docs : undefined,
			citations: docs.length > 0
406
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
407

408
		if (res && res.ok) {
409
410
			console.log('controller', controller);

Rohit Das's avatar
Rohit Das committed
411
412
413
414
415
416
417
418
419
420
			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;
421
422
423

					if (stopResponseFlag) {
						controller.abort('User: Stop Response');
Timothy J. Baek's avatar
Timothy J. Baek committed
424
						await cancelOllamaRequest(localStorage.token, currentRequestId);
425
426
427
428
					}

					currentRequestId = null;

Rohit Das's avatar
Rohit Das committed
429
430
					break;
				}
431

Rohit Das's avatar
Rohit Das committed
432
433
				try {
					let lines = value.split('\n');
434

Rohit Das's avatar
Rohit Das committed
435
436
437
438
					for (const line of lines) {
						if (line !== '') {
							console.log(line);
							let data = JSON.parse(line);
Timothy J. Baek's avatar
Timothy J. Baek committed
439

440
441
442
443
444
							if ('citations' in data) {
								responseMessage.citations = data.citations;
								continue;
							}

Rohit Das's avatar
Rohit Das committed
445
446
447
							if ('detail' in data) {
								throw data;
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
448

449
450
451
452
453
454
455
456
457
458
459
							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
460
								} else {
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
									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
480
									messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
481

482
483
484
485
486
487
488
									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
489
												: `${model}`,
490
491
											{
												body: responseMessage.content,
492
												icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
493
494
495
496
497
498
499
											}
										);
									}

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

									if ($settings.responseAutoPlayback) {
Timothy J. Baek's avatar
Timothy J. Baek committed
502
										await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
503
504
										document.getElementById(`speak-button-${responseMessage.id}`)?.click();
									}
Timothy J. Baek's avatar
Timothy J. Baek committed
505
								}
506
507
508
							}
						}
					}
Rohit Das's avatar
Rohit Das committed
509
510
511
512
513
514
				} catch (error) {
					console.log(error);
					if ('detail' in error) {
						toast.error(error.detail);
					}
					break;
515
				}
Rohit Das's avatar
Rohit Das committed
516
517

				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
518
					scrollToBottom();
519
				}
520
			}
521

522
			if ($chatId == _chatId) {
523
524
525
526
527
528
529
				if ($settings.saveChatHistory ?? true) {
					chat = await updateChatById(localStorage.token, _chatId, {
						messages: messages,
						history: history
					});
					await chats.set(await getChatList(localStorage.token));
				}
530
			}
531
532
533
		} else {
			if (res !== null) {
				const error = await res.json();
534
				console.log(error);
535
536
				if ('detail' in error) {
					toast.error(error.detail);
537
					responseMessage.content = error.detail;
538
539
				} else {
					toast.error(error.error);
540
					responseMessage.content = error.error;
541
				}
542
			} else {
Ased Mammad's avatar
Ased Mammad committed
543
544
545
546
547
548
				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'
				});
549
550
			}

551
			responseMessage.error = true;
Ased Mammad's avatar
Ased Mammad committed
552
553
554
			responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
				provider: 'Ollama'
			});
555
556
			responseMessage.done = true;
			messages = messages;
557
558
559
560
		}

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

562
		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
563
			scrollToBottom();
564
		}
565

566
		if (messages.length == 2 && messages.at(1).content !== '') {
Timothy J. Baek's avatar
Timothy J. Baek committed
567
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
568
569
			const _title = await generateChatTitle(userPrompt);
			await setChatTitle(_chatId, _title);
570
571
572
		}
	};

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

Timothy J. Baek's avatar
Timothy J. Baek committed
576
577
578
579
580
581
582
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

583
584
		console.log(docs);

Timothy J. Baek's avatar
Timothy J. Baek committed
585
		scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
586

587
		const [res, controller] = await generateOpenAIChatCompletion(
588
589
590
591
592
593
			localStorage.token,
			{
				model: model.id,
				stream: true,
				messages: [
					$settings.system
Timothy J. Baek's avatar
Timothy J. Baek committed
594
						? {
595
596
								role: 'system',
								content: $settings.system
Timothy J. Baek's avatar
Timothy J. Baek committed
597
						  }
598
						: undefined,
Danny Liu's avatar
Danny Liu committed
599
					...messages
600
601
602
603
				]
					.filter((message) => message)
					.map((message, idx, arr) => ({
						role: message.role,
604
605
						...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) &&
						message.role === 'user'
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
							? {
									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,
631
632
633
634
635
636
				stop:
					$settings?.options?.stop ?? undefined
						? $settings?.options?.stop.map((str) =>
								decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
						  )
						: undefined,
637
638
639
640
				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,
Timothy J. Baek's avatar
Timothy J. Baek committed
641
				max_tokens: $settings?.options?.num_predict ?? undefined,
642
643
				docs: docs.length > 0 ? docs : undefined,
				citations: docs.length > 0
644
			},
Timothy J. Baek's avatar
Timothy J. Baek committed
645
			model?.source?.toLowerCase() === 'litellm'
Timothy J. Baek's avatar
Timothy J. Baek committed
646
647
				? `${LITELLM_API_BASE_URL}/v1`
				: `${OPENAI_API_BASE_URL}`
648
		);
649

Timothy J. Baek's avatar
Timothy J. Baek committed
650
651
652
653
654
		// Wait until history/message have been updated
		await tick();

		scrollToBottom();

655
656
		if (res && res.ok && res.body) {
			const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
657
658

			for await (const update of textStream) {
659
				const { value, done, citations } = update;
Timothy J. Baek's avatar
Timothy J. Baek committed
660
661
662
				if (done || stopResponseFlag || _chatId !== $chatId) {
					responseMessage.done = true;
					messages = messages;
663
664
665
666
667

					if (stopResponseFlag) {
						controller.abort('User: Stop Response');
					}

Timothy J. Baek's avatar
Timothy J. Baek committed
668
669
					break;
				}
670

671
672
673
674
675
				if (citations) {
					responseMessage.citations = citations;
					continue;
				}

676
677
678
679
680
				if (responseMessage.content == '' && value == '\n') {
					continue;
				} else {
					responseMessage.content += value;
					messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
681
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
682

Timothy J. Baek's avatar
Timothy J. Baek committed
683
684
685
				if ($settings.notificationEnabled && !document.hasFocus()) {
					const notification = new Notification(`OpenAI ${model}`, {
						body: responseMessage.content,
686
						icon: `${WEBUI_BASE_URL}/static/favicon.png`
Timothy J. Baek's avatar
Timothy J. Baek committed
687
					});
Timothy J. Baek's avatar
Timothy J. Baek committed
688
689
				}

Timothy J. Baek's avatar
Timothy J. Baek committed
690
691
692
				if ($settings.responseAutoCopy) {
					copyToClipboard(responseMessage.content);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
693

Timothy J. Baek's avatar
Timothy J. Baek committed
694
				if ($settings.responseAutoPlayback) {
Timothy J. Baek's avatar
Timothy J. Baek committed
695
					await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
696
697
698
					document.getElementById(`speak-button-${responseMessage.id}`)?.click();
				}

699
				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
700
					scrollToBottom();
701
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
702
			}
703

Timothy J. Baek's avatar
Timothy J. Baek committed
704
			if ($chatId == _chatId) {
705
706
707
708
709
710
711
				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
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
			}
		} 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;
					}
728
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
729
			} else {
Ased Mammad's avatar
Ased Mammad committed
730
				toast.error(
731
732
733
					$i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
						provider: model.name ?? model.id
					})
Ased Mammad's avatar
Ased Mammad committed
734
735
				);
				responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
736
					provider: model.name ?? model.id
Ased Mammad's avatar
Ased Mammad committed
737
				});
738
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
739
740

			responseMessage.error = true;
Ased Mammad's avatar
Ased Mammad committed
741
			responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
742
				provider: model.name ?? model.id
Ased Mammad's avatar
Ased Mammad committed
743
			});
Timothy J. Baek's avatar
Timothy J. Baek committed
744
745
746
747
748
749
750
751
			responseMessage.done = true;
			messages = messages;
		}

		stopResponseFlag = false;
		await tick();

		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
752
			scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
753
754
755
756
		}

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

758
759
			const _title = await generateChatTitle(userPrompt);
			await setChatTitle(_chatId, _title);
760
		}
761
762
	};

763
764
765
766
767
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

768
	const regenerateResponse = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
769
		console.log('regenerateResponse');
770
771
772
		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
773

774
775
			let userMessage = messages.at(-1);
			let userPrompt = userMessage.content;
776

Timothy J. Baek's avatar
Timothy J. Baek committed
777
			await sendPrompt(userPrompt, userMessage.id);
Timothy J. Baek's avatar
Timothy J. Baek committed
778
		}
779
	};
780

Timothy J. Baek's avatar
Timothy J. Baek committed
781
782
783
784
785
786
	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];
787
788
789
			responseMessage.done = false;
			await tick();

Timothy J. Baek's avatar
Timothy J. Baek committed
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
			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
807
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
808
		} else {
Ased Mammad's avatar
Ased Mammad committed
809
			toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
Timothy J. Baek's avatar
Timothy J. Baek committed
810
811
812
		}
	};

813
814
815
816
817
818
819
820
821
822
823
	const generateChatTitle = async (userPrompt) => {
		if ($settings?.title?.auto ?? true) {
			const model = $models.find((model) => model.id === selectedModels[0]);

			const titleModelId =
				model?.external ?? false
					? $settings?.title?.modelExternal ?? selectedModels[0]
					: $settings?.title?.model ?? selectedModels[0];
			const titleModel = $models.find((model) => model.id === titleModelId);

			console.log(titleModel);
Timothy J. Baek's avatar
Timothy J. Baek committed
824
825
			const title = await generateTitle(
				localStorage.token,
826
				$settings?.title?.prompt ??
Ased Mammad's avatar
Ased Mammad committed
827
828
829
					$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}}',
830
831
832
				titleModelId,
				userPrompt,
				titleModel?.external ?? false
833
					? titleModel?.source?.toLowerCase() === 'litellm'
834
835
836
						? `${LITELLM_API_BASE_URL}/v1`
						: `${OPENAI_API_BASE_URL}`
					: `${OLLAMA_API_BASE_URL}/v1`
Timothy J. Baek's avatar
Timothy J. Baek committed
837
838
			);

839
			return title;
840
		} else {
841
			return `${userPrompt}`;
842
843
844
		}
	};

845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
	// TODO: Add support for adding all the user's messages as context, and not just the last message
	const generateChatSearchQuery = async (userPrompt: string) => {
		const model = $models.find((model) => model.id === selectedModels[0]);

		// TODO: rename titleModel to taskModel - this is the model used for non-chat tasks (e.g. title generation, search query generation)
		const titleModelId =
			model?.external ?? false
				? $settings?.title?.modelExternal ?? selectedModels[0]
				: $settings?.title?.model ?? selectedModels[0];
		const titleModel = $models.find((model) => model.id === titleModelId);

		console.log(titleModel);
		return await generateSearchQuery(
			localStorage.token,
			titleModelId,
			userPrompt,
			titleModel?.external ?? false
				? titleModel?.source?.toLowerCase() === 'litellm'
					? `${LITELLM_API_BASE_URL}/v1`
					: `${OPENAI_API_BASE_URL}`
				: `${OLLAMA_API_BASE_URL}/v1`
		);
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
869
870
871
872
873
874
875
876
877
878
879
	const setChatTitle = async (_chatId, _title) => {
		if (_chatId === $chatId) {
			title = _title;
		}

		if ($settings.saveChatHistory ?? true) {
			chat = await updateChatById(localStorage.token, _chatId, { title: _title });
			await chats.set(await getChatList(localStorage.token));
		}
	};

880
881
882
883
884
885
886
887
888
	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
889
890
891
892

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

		_tags.set(await getAllChatTags(localStorage.token));
895
896
897
898
899
	};

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

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

		_tags.set(await getAllChatTags(localStorage.token));
906
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
907
908
</script>

909
910
<svelte:head>
	<title>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
911
912
913
		{title
			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
			: `${$WEBUI_NAME}`}
914
915
916
	</title>
</svelte:head>

Timothy J. Baek's avatar
Timothy J. Baek committed
917
918
919
920
921
<div
	class="min-h-screen max-h-screen {$showSidebar
		? 'lg:max-w-[calc(100%-260px)]'
		: ''} w-full max-w-full flex flex-col"
>
Timothy J. Baek's avatar
Timothy J. Baek committed
922
923
924
925
926
	<Navbar
		{title}
		bind:selectedModels
		bind:showModelSelector
		shareEnabled={messages.length > 0}
Timothy J. Baek's avatar
Timothy J. Baek committed
927
		{chat}
Timothy J. Baek's avatar
Timothy J. Baek committed
928
929
		{initNewChat}
	/>
Timothy J. Baek's avatar
Timothy J. Baek committed
930
931
	<div class="flex flex-col flex-auto">
		<div
Timothy J. Baek's avatar
Timothy J. Baek committed
932
			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
933
			id="messages-container"
934
			bind:this={messagesContainerElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
935
			on:scroll={(e) => {
936
937
				autoScroll =
					messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
938
					messagesContainerElement.clientHeight + 5;
Timothy J. Baek's avatar
Timothy J. Baek committed
939
940
			}}
		>
Timothy J. Baek's avatar
Timothy J. Baek committed
941
			<div class=" h-full w-full flex flex-col pt-2 pb-4">
Timothy J. Baek's avatar
Timothy J. Baek committed
942
943
944
945
946
947
948
949
				<Messages
					chatId={$chatId}
					{selectedModels}
					{selectedModelfiles}
					{processing}
					bind:history
					bind:messages
					bind:autoScroll
Timothy J. Baek's avatar
Timothy J. Baek committed
950
					bind:prompt
Timothy J. Baek's avatar
Timothy J. Baek committed
951
					bottomPadding={files.length > 0}
Timothy J. Baek's avatar
Timothy J. Baek committed
952
953
					suggestionPrompts={selectedModelfile?.suggestionPrompts ??
						$config.default_prompt_suggestions}
Timothy J. Baek's avatar
Timothy J. Baek committed
954
955
956
957
958
					{sendPrompt}
					{continueGeneration}
					{regenerateResponse}
				/>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
959
		</div>
960
961
	</div>
</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
962
963
964
965
966
967

<MessageInput
	bind:files
	bind:prompt
	bind:autoScroll
	bind:selectedModel={atSelectedModel}
968
	bind:useWebSearch
Timothy J. Baek's avatar
Timothy J. Baek committed
969
970
971
972
	{messages}
	{submitPrompt}
	{stopResponse}
/>