+page.svelte 25.3 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
		tags as _tags,
19
20
		showSidebar,
		type Model
Timothy J. Baek's avatar
Timothy J. Baek committed
21
	} from '$lib/stores';
22
	import { copyToClipboard, splitStream } from '$lib/utils';
Timothy J. Baek's avatar
Timothy J. Baek committed
23

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

	import MessageInput from '$lib/components/chat/MessageInput.svelte';
	import Messages from '$lib/components/chat/Messages.svelte';
	import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
40
	import Navbar from '$lib/components/layout/Navbar.svelte';
41
	import { RAGTemplate } from '$lib/utils/rag';
42
	import { LITELLM_API_BASE_URL, OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants';
43
	import { WEBUI_BASE_URL } from '$lib/constants';
44
	import { createOpenAITextStream } from '$lib/apis/streaming';
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 = [''];
57
	let atSelectedModel: Model | undefined;
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
		await Promise.all(
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
			(modelId
				? [modelId]
				: atSelectedModel !== undefined
				? [atSelectedModel.id]
				: selectedModels
			).map(async (modelId) => {
				console.log('modelId', modelId);
				const model = $models.filter((m) => m.id === modelId).at(0);

				if (model) {
					// If there are image files, check if model is vision capable
					const hasImages = messages.some((message) =>
						message.files?.some((file) => file.type === 'image')
					);
					if (hasImages && !(model.custom_info?.params.vision_capable ?? true)) {
						toast.error(
							$i18n.t('Model {{modelName}} is not vision capable', {
								modelName: model.custom_info?.name ?? model.name ?? model.id
							})
278
						);
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
					// Create response message
					let responseMessageId = uuidv4();
					let responseMessage = {
						parentId: parentId,
						id: responseMessageId,
						childrenIds: [],
						role: 'assistant',
						content: '',
						model: model.id,
						modelName: model.custom_info?.name ?? model.name ?? model.id,
						timestamp: Math.floor(Date.now() / 1000) // Unix epoch
					};

					// Add message to history and Set currentId to messageId
					history.messages[responseMessageId] = responseMessage;
					history.currentId = responseMessageId;

					// Append messageId to childrenIds of parent message
					if (parentId !== null) {
						history.messages[parentId].childrenIds = [
							...history.messages[parentId].childrenIds,
							responseMessageId
						];
Timothy J. Baek's avatar
Timothy J. Baek committed
304
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
305

306
307
308
309
					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
310
					}
311
312
				} else {
					toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
Timothy J. Baek's avatar
Timothy J. Baek committed
313
				}
314
			})
Timothy J. Baek's avatar
Timothy J. Baek committed
315
		);
316

Timothy J. Baek's avatar
Timothy J. Baek committed
317
		await chats.set(await getChatList(localStorage.token));
318
319
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
320
	const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
321
		const modelName = model.custom_info?.name ?? model.name ?? model.id;
322
		model = model.id;
Timothy J. Baek's avatar
Timothy J. Baek committed
323
		const responseMessage = history.messages[responseMessageId];
Timothy J. Baek's avatar
Timothy J. Baek committed
324

Timothy J. Baek's avatar
Timothy J. Baek committed
325
		// Wait until history/message have been updated
Timothy J. Baek's avatar
Timothy J. Baek committed
326
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
327
328

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
329
		scrollToBottom();
330

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

				return baseMessage;
			});
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376

		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
377
378
379
380
381
382
383
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

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

402
		if (res && res.ok) {
403
404
			console.log('controller', controller);

Rohit Das's avatar
Rohit Das committed
405
406
407
408
409
410
411
412
413
414
			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;
415
416
417

					if (stopResponseFlag) {
						controller.abort('User: Stop Response');
Timothy J. Baek's avatar
Timothy J. Baek committed
418
						await cancelOllamaRequest(localStorage.token, currentRequestId);
419
420
421
422
					}

					currentRequestId = null;

Rohit Das's avatar
Rohit Das committed
423
424
					break;
				}
425

Rohit Das's avatar
Rohit Das committed
426
427
				try {
					let lines = value.split('\n');
428

Rohit Das's avatar
Rohit Das committed
429
430
431
432
					for (const line of lines) {
						if (line !== '') {
							console.log(line);
							let data = JSON.parse(line);
Timothy J. Baek's avatar
Timothy J. Baek committed
433

434
435
436
437
438
							if ('citations' in data) {
								responseMessage.citations = data.citations;
								continue;
							}

Rohit Das's avatar
Rohit Das committed
439
440
441
							if ('detail' in data) {
								throw data;
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
442

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

476
477
478
479
480
481
482
									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
483
												: `${model}`,
484
485
											{
												body: responseMessage.content,
486
												icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
487
488
489
490
491
492
493
											}
										);
									}

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

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

				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
512
					scrollToBottom();
513
				}
514
			}
515

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

545
			responseMessage.error = true;
Ased Mammad's avatar
Ased Mammad committed
546
547
548
			responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
				provider: 'Ollama'
			});
549
550
			responseMessage.done = true;
			messages = messages;
551
552
553
554
		}

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

556
		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
557
			scrollToBottom();
558
		}
559

560
		if (messages.length == 2 && messages.at(1).content !== '') {
Timothy J. Baek's avatar
Timothy J. Baek committed
561
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
562
563
			const _title = await generateChatTitle(userPrompt);
			await setChatTitle(_chatId, _title);
564
565
566
		}
	};

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

Timothy J. Baek's avatar
Timothy J. Baek committed
570
571
572
573
574
575
576
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

577
578
		console.log(docs);

Timothy J. Baek's avatar
Timothy J. Baek committed
579
		scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
580

581
582
583
584
585
586
587
588
		try {
			const [res, controller] = await generateOpenAIChatCompletion(
				localStorage.token,
				{
					model: model.id,
					stream: true,
					messages: [
						$settings.system
589
							? {
590
591
									role: 'system',
									content: $settings.system
592
							  }
593
594
595
596
597
598
599
600
601
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
							: 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
646

647
648
			// Wait until history/message have been updated
			await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
649

650
			scrollToBottom();
651

652
653
			if (res && res.ok && res.body) {
				const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
654

655
656
657
658
659
				for await (const update of textStream) {
					const { value, done, citations, error } = update;
					if (error) {
						await handleOpenAIError(error, null, model, responseMessage);
						break;
660
					}
661
662
663
					if (done || stopResponseFlag || _chatId !== $chatId) {
						responseMessage.done = true;
						messages = messages;
664

665
666
667
						if (stopResponseFlag) {
							controller.abort('User: Stop Response');
						}
668

669
670
						break;
					}
671

672
673
674
675
					if (citations) {
						responseMessage.citations = citations;
						continue;
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
676

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

684
685
686
687
688
689
					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
690

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

695
696
697
698
					if ($settings.responseAutoPlayback) {
						await tick();
						document.getElementById(`speak-button-${responseMessage.id}`)?.click();
					}
699

700
701
702
					if (autoScroll) {
						scrollToBottom();
					}
703
				}
704
705
706
707
708
709
710
711

				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
712
					}
713
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
714
			} else {
715
				await handleOpenAIError(null, res, model, responseMessage);
716
			}
717
718
		} catch (error) {
			await handleOpenAIError(error, null, model, responseMessage);
Timothy J. Baek's avatar
Timothy J. Baek committed
719
		}
720
		messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
721
722
723
724
725

		stopResponseFlag = false;
		await tick();

		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
726
			scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
727
728
729
730
		}

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

732
733
			const _title = await generateChatTitle(userPrompt);
			await setChatTitle(_chatId, _title);
734
		}
735
736
	};

737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
	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}}.`, {
766
				provider: model.custom_info?.name ?? model.name ?? model.id
767
768
769
770
771
772
773
774
			}) +
			'\n' +
			errorMessage;
		responseMessage.done = true;

		messages = messages;
	};

775
776
777
778
779
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
780
	const regenerateResponse = async (message) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
781
		console.log('regenerateResponse');
Timothy J. Baek's avatar
Timothy J. Baek committed
782

Timothy J. Baek's avatar
Timothy J. Baek committed
783
784
		if (messages.length != 0) {
			let userMessage = history.messages[message.parentId];
785
			let userPrompt = userMessage.content;
786

Timothy J. Baek's avatar
Timothy J. Baek committed
787
788
789
790
791
			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
792
		}
793
	};
794

Timothy J. Baek's avatar
Timothy J. Baek committed
795
796
797
798
799
800
	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];
801
802
803
			responseMessage.done = false;
			await tick();

Timothy J. Baek's avatar
Timothy J. Baek committed
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
			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
821
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
822
		} else {
Ased Mammad's avatar
Ased Mammad committed
823
			toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
Timothy J. Baek's avatar
Timothy J. Baek committed
824
825
826
		}
	};

827
828
829
830
831
832
833
834
835
836
837
	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
838
839
			const title = await generateTitle(
				localStorage.token,
840
				$settings?.title?.prompt ??
Ased Mammad's avatar
Ased Mammad committed
841
842
843
					$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}}',
844
845
846
				titleModelId,
				userPrompt,
				titleModel?.external ?? false
847
					? titleModel?.source?.toLowerCase() === 'litellm'
848
849
850
						? `${LITELLM_API_BASE_URL}/v1`
						: `${OPENAI_API_BASE_URL}`
					: `${OLLAMA_API_BASE_URL}/v1`
Timothy J. Baek's avatar
Timothy J. Baek committed
851
852
			);

853
			return title;
854
		} else {
855
			return `${userPrompt}`;
856
857
858
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
859
860
861
862
863
864
865
866
867
868
869
	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));
		}
	};

870
871
872
873
874
875
876
877
878
	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
879
880
881
882

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

		_tags.set(await getAllChatTags(localStorage.token));
885
886
887
888
889
	};

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

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

		_tags.set(await getAllChatTags(localStorage.token));
896
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
897
898
</script>

899
900
<svelte:head>
	<title>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
901
902
903
		{title
			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
			: `${$WEBUI_NAME}`}
904
905
906
	</title>
</svelte:head>

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

<MessageInput
	bind:files
	bind:prompt
	bind:autoScroll
	bind:selectedModel={atSelectedModel}
958
	{selectedModels}
Timothy J. Baek's avatar
Timothy J. Baek committed
959
960
961
962
	{messages}
	{submitPrompt}
	{stopResponse}
/>