+page.svelte 25.7 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
	import { queryCollection, queryDoc } from '$lib/apis/rag';
34
	import { generateOpenAIChatCompletion, 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';
Timothy J. Baek's avatar
Timothy J. Baek committed
44
	import { queryMemory } from '$lib/apis/memories';
Ased Mammad's avatar
Ased Mammad committed
45
46
47

	const i18n = getContext('i18n');

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

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

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

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

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

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

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

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

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

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

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

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

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

138
139
140
141
142
143
144
145
		if ($page.url.searchParams.get('q')) {
			prompt = $page.url.searchParams.get('q') ?? '';
			if (prompt) {
				await tick();
				submitPrompt(prompt);
			}
		}

Timothy J. Baek's avatar
Timothy J. Baek committed
146
147
148
149
		selectedModels = selectedModels.map((modelId) =>
			$models.map((m) => m.id).includes(modelId) ? modelId : ''
		);

150
151
152
153
		let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
		settings.set({
			..._settings
		});
154
155
156

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

Timothy J. Baek's avatar
Timothy J. Baek committed
159
160
	const scrollToBottom = async () => {
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
161
162
163
		if (messagesContainerElement) {
			messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
164
165
	};

166
167
168
169
	//////////////////////////
	// Ollama functions
	//////////////////////////

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

Timothy J. Baek's avatar
Timothy J. Baek committed
173
174
175
176
		selectedModels = selectedModels.map((modelId) =>
			$models.map((m) => m.id).includes(modelId) ? modelId : ''
		);

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

			// 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,
227
						title: $i18n.t('New Chat'),
228
229
230
231
232
233
234
						models: selectedModels,
						system: $settings.system ?? undefined,
						options: {
							...($settings.options ?? {})
						},
						messages: messages,
						history: history,
Timothy J. Baek's avatar
Timothy J. Baek committed
235
						tags: [],
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
						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);
		}
	};

255
	const sendPrompt = async (prompt, parentId, modelId = null) => {
256
		const _chatId = JSON.parse(JSON.stringify($chatId));
257

Timothy J. Baek's avatar
Timothy J. Baek committed
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
		let userContext = null;

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

			if (res) {
				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;
				}, []);

				console.log(userContext);
			}
		}

Timothy J. Baek's avatar
Timothy J. Baek committed
278
		await Promise.all(
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
			(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
Timothy J. Baek committed
294
							userContext: userContext,
295
296
297
298
299
300
301
302
303
304
305
306
307
308
							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
309

310
311
312
313
314
315
316
						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
317
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
318
				}
319
			)
Timothy J. Baek's avatar
Timothy J. Baek committed
320
		);
321

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

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

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

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
333
		scrollToBottom();
334

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

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

		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
384
385
386
387
388
389
390
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

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

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

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

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

					currentRequestId = null;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

584
585
		console.log(docs);

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

588
589
590
591
592
593
594
		try {
			const [res, controller] = await generateOpenAIChatCompletion(
				localStorage.token,
				{
					model: model.id,
					stream: true,
					messages: [
Timothy J. Baek's avatar
Timothy J. Baek committed
595
						$settings.system || responseMessage?.userContext
596
							? {
597
									role: 'system',
Timothy J. Baek's avatar
Timothy J. Baek committed
598
599
600
601
									content:
										$settings.system + (responseMessage?.userContext ?? null)
											? `\n\nUser Context:\n${responseMessage.userContext.join('\n')}`
											: ''
602
							  }
603
604
605
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
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
							: 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
656

657
658
			// Wait until history/message have been updated
			await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
659

660
			scrollToBottom();
661

662
663
			if (res && res.ok && res.body) {
				const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
664

665
666
667
668
669
				for await (const update of textStream) {
					const { value, done, citations, error } = update;
					if (error) {
						await handleOpenAIError(error, null, model, responseMessage);
						break;
670
					}
671
672
673
					if (done || stopResponseFlag || _chatId !== $chatId) {
						responseMessage.done = true;
						messages = messages;
674

675
676
677
						if (stopResponseFlag) {
							controller.abort('User: Stop Response');
						}
678

679
680
						break;
					}
681

682
683
684
685
					if (citations) {
						responseMessage.citations = citations;
						continue;
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
686

687
688
689
690
691
692
					if (responseMessage.content == '' && value == '\n') {
						continue;
					} else {
						responseMessage.content += value;
						messages = messages;
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
693

694
695
696
697
698
699
					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
700

701
702
703
					if ($settings.responseAutoCopy) {
						copyToClipboard(responseMessage.content);
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
704

705
706
707
708
					if ($settings.responseAutoPlayback) {
						await tick();
						document.getElementById(`speak-button-${responseMessage.id}`)?.click();
					}
709

710
711
712
					if (autoScroll) {
						scrollToBottom();
					}
713
				}
714
715
716
717
718
719
720
721

				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
722
					}
723
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
724
			} else {
725
				await handleOpenAIError(null, res, model, responseMessage);
726
			}
727
728
		} catch (error) {
			await handleOpenAIError(error, null, model, responseMessage);
Timothy J. Baek's avatar
Timothy J. Baek committed
729
		}
730
		messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
731
732
733
734
735

		stopResponseFlag = false;
		await tick();

		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
736
			scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
737
738
739
740
		}

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

742
743
			const _title = await generateChatTitle(userPrompt);
			await setChatTitle(_chatId, _title);
744
		}
745
746
	};

747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
	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;
	};

785
786
787
788
789
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
790
	const regenerateResponse = async (message) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
791
		console.log('regenerateResponse');
Timothy J. Baek's avatar
Timothy J. Baek committed
792

Timothy J. Baek's avatar
Timothy J. Baek committed
793
794
		if (messages.length != 0) {
			let userMessage = history.messages[message.parentId];
795
			let userPrompt = userMessage.content;
796

Timothy J. Baek's avatar
Timothy J. Baek committed
797
798
799
800
801
			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
802
		}
803
	};
804

Timothy J. Baek's avatar
Timothy J. Baek committed
805
806
807
808
809
810
	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];
811
812
813
			responseMessage.done = false;
			await tick();

Timothy J. Baek's avatar
Timothy J. Baek committed
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
			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
831
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
832
		} else {
Ased Mammad's avatar
Ased Mammad committed
833
			toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
Timothy J. Baek's avatar
Timothy J. Baek committed
834
835
836
		}
	};

837
838
839
840
841
842
843
844
845
846
847
	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
848
849
			const title = await generateTitle(
				localStorage.token,
850
				$settings?.title?.prompt ??
Ased Mammad's avatar
Ased Mammad committed
851
852
853
					$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}}',
854
855
856
				titleModelId,
				userPrompt,
				titleModel?.external ?? false
857
					? titleModel?.source?.toLowerCase() === 'litellm'
858
859
860
						? `${LITELLM_API_BASE_URL}/v1`
						: `${OPENAI_API_BASE_URL}`
					: `${OLLAMA_API_BASE_URL}/v1`
Timothy J. Baek's avatar
Timothy J. Baek committed
861
862
			);

863
			return title;
864
		} else {
865
			return `${userPrompt}`;
866
867
868
		}
	};

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
<div
	class="min-h-screen max-h-screen {$showSidebar
Timothy J. Baek's avatar
Timothy J. Baek committed
919
		? 'md:max-w-[calc(100%-260px)]'
Timothy J. Baek's avatar
Timothy J. Baek committed
920
921
		: ''} 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
968
969
970
971

<MessageInput
	bind:files
	bind:prompt
	bind:autoScroll
	bind:selectedModel={atSelectedModel}
	{messages}
	{submitPrompt}
	{stopResponse}
/>