+page.svelte 24 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';
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
60
61
62
63
	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;
64

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

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

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

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

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

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

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

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

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

137
138
139
140
141
142
143
144
		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
145
146
147
148
		selectedModels = selectedModels.map((modelId) =>
			$models.map((m) => m.id).includes(modelId) ? modelId : ''
		);

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

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

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

165
166
167
168
	//////////////////////////
	// Ollama functions
	//////////////////////////

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

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

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

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

253
254
	const sendPrompt = async (prompt, parentId) => {
		const _chatId = JSON.parse(JSON.stringify($chatId));
255

Timothy J. Baek's avatar
Timothy J. Baek committed
256
		await Promise.all(
Timothy J. Baek's avatar
Timothy J. Baek committed
257
258
			(atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(async (modelId) => {
				console.log('modelId', modelId);
259
				const model = $models.filter((m) => m.id === modelId).at(0);
Timothy J. Baek's avatar
Timothy J. Baek committed
260

Timothy J. Baek's avatar
Timothy J. Baek committed
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
				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
285

Timothy J. Baek's avatar
Timothy J. Baek committed
286
287
288
289
290
					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
291
				} else {
Ased Mammad's avatar
Ased Mammad committed
292
					toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
Timothy J. Baek's avatar
Timothy J. Baek committed
293
294
295
				}
			})
		);
296

Timothy J. Baek's avatar
Timothy J. Baek committed
297
		await chats.set(await getChatList(localStorage.token));
298
299
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
300
	const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
301
		model = model.id;
Timothy J. Baek's avatar
Timothy J. Baek committed
302
		const responseMessage = history.messages[responseMessageId];
Timothy J. Baek's avatar
Timothy J. Baek committed
303

Timothy J. Baek's avatar
Timothy J. Baek committed
304
		// Wait until history/message have been updated
Timothy J. Baek's avatar
Timothy J. Baek committed
305
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
306
307

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
308
		scrollToBottom();
309

310
311
312
313
314
315
316
		const messagesBody = [
			$settings.system
				? {
						role: 'system',
						content: $settings.system
				  }
				: undefined,
Danny Liu's avatar
Danny Liu committed
317
			...messages
318
319
		]
			.filter((message) => message)
Timothy J. Baek's avatar
Timothy J. Baek committed
320
321
322
323
324
325
326
327
328
329
330
331
332
			.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
333
				if (imageUrls && imageUrls.length > 0 && message.role === 'user') {
Timothy J. Baek's avatar
Timothy J. Baek committed
334
335
336
337
338
					baseMessage.images = imageUrls;
				}

				return baseMessage;
			});
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355

		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
356
357
358
359
360
361
362
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

363
		const [res, controller] = await generateChatCompletion(localStorage.token, {
364
			model: model,
365
			messages: messagesBody,
366
			options: {
367
368
369
370
371
372
373
				...($settings.options ?? {}),
				stop:
					$settings?.options?.stop ?? undefined
						? $settings.options.stop.map((str) =>
								decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
						  )
						: undefined
374
			},
Zohaib Rauf's avatar
Zohaib Rauf committed
375
			format: $settings.requestFormat ?? undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
376
			keep_alive: $settings.keepAlive ?? undefined,
377
378
			docs: docs.length > 0 ? docs : undefined,
			citations: docs.length > 0
379
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
380

381
		if (res && res.ok) {
382
383
			console.log('controller', controller);

Rohit Das's avatar
Rohit Das committed
384
385
386
387
388
389
390
391
392
393
			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;
394
395
396

					if (stopResponseFlag) {
						controller.abort('User: Stop Response');
Timothy J. Baek's avatar
Timothy J. Baek committed
397
						await cancelOllamaRequest(localStorage.token, currentRequestId);
398
399
400
401
					}

					currentRequestId = null;

Rohit Das's avatar
Rohit Das committed
402
403
					break;
				}
404

Rohit Das's avatar
Rohit Das committed
405
406
				try {
					let lines = value.split('\n');
407

Rohit Das's avatar
Rohit Das committed
408
409
410
411
					for (const line of lines) {
						if (line !== '') {
							console.log(line);
							let data = JSON.parse(line);
Timothy J. Baek's avatar
Timothy J. Baek committed
412

413
414
415
416
417
							if ('citations' in data) {
								responseMessage.citations = data.citations;
								continue;
							}

Rohit Das's avatar
Rohit Das committed
418
419
420
							if ('detail' in data) {
								throw data;
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
421

422
423
424
425
426
427
428
429
430
431
432
							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
433
								} else {
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
									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
453
									messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
454

455
456
457
458
459
460
461
									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
462
												: `${model}`,
463
464
											{
												body: responseMessage.content,
465
												icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
466
467
468
469
470
471
472
											}
										);
									}

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

									if ($settings.responseAutoPlayback) {
Timothy J. Baek's avatar
Timothy J. Baek committed
475
										await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
476
477
										document.getElementById(`speak-button-${responseMessage.id}`)?.click();
									}
Timothy J. Baek's avatar
Timothy J. Baek committed
478
								}
479
480
481
							}
						}
					}
Rohit Das's avatar
Rohit Das committed
482
483
484
485
486
487
				} catch (error) {
					console.log(error);
					if ('detail' in error) {
						toast.error(error.detail);
					}
					break;
488
				}
Rohit Das's avatar
Rohit Das committed
489
490

				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
491
					scrollToBottom();
492
				}
493
			}
494

495
			if ($chatId == _chatId) {
496
497
498
499
500
501
502
				if ($settings.saveChatHistory ?? true) {
					chat = await updateChatById(localStorage.token, _chatId, {
						messages: messages,
						history: history
					});
					await chats.set(await getChatList(localStorage.token));
				}
503
			}
504
505
506
		} else {
			if (res !== null) {
				const error = await res.json();
507
				console.log(error);
508
509
				if ('detail' in error) {
					toast.error(error.detail);
510
					responseMessage.content = error.detail;
511
512
				} else {
					toast.error(error.error);
513
					responseMessage.content = error.error;
514
				}
515
			} else {
Ased Mammad's avatar
Ased Mammad committed
516
517
518
519
520
521
				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'
				});
522
523
			}

524
			responseMessage.error = true;
Ased Mammad's avatar
Ased Mammad committed
525
526
527
			responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
				provider: 'Ollama'
			});
528
529
			responseMessage.done = true;
			messages = messages;
530
531
532
533
		}

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

535
		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
536
			scrollToBottom();
537
		}
538

539
		if (messages.length == 2 && messages.at(1).content !== '') {
Timothy J. Baek's avatar
Timothy J. Baek committed
540
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
541
542
			const _title = await generateChatTitle(userPrompt);
			await setChatTitle(_chatId, _title);
543
544
545
		}
	};

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

Timothy J. Baek's avatar
Timothy J. Baek committed
549
550
551
552
553
554
555
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

556
557
		console.log(docs);

Timothy J. Baek's avatar
Timothy J. Baek committed
558
		scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
559

560
		const [res, controller] = await generateOpenAIChatCompletion(
561
562
563
564
565
566
			localStorage.token,
			{
				model: model.id,
				stream: true,
				messages: [
					$settings.system
Timothy J. Baek's avatar
Timothy J. Baek committed
567
						? {
568
569
								role: 'system',
								content: $settings.system
Timothy J. Baek's avatar
Timothy J. Baek committed
570
						  }
571
						: undefined,
Danny Liu's avatar
Danny Liu committed
572
					...messages
573
574
575
576
				]
					.filter((message) => message)
					.map((message, idx, arr) => ({
						role: message.role,
577
578
						...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) &&
						message.role === 'user'
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
							? {
									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,
604
605
606
607
608
609
				stop:
					$settings?.options?.stop ?? undefined
						? $settings?.options?.stop.map((str) =>
								decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
						  )
						: undefined,
610
611
612
613
				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
614
				max_tokens: $settings?.options?.num_predict ?? undefined,
615
616
				docs: docs.length > 0 ? docs : undefined,
				citations: docs.length > 0
617
			},
Timothy J. Baek's avatar
Timothy J. Baek committed
618
			model?.source?.toLowerCase() === 'litellm'
Timothy J. Baek's avatar
Timothy J. Baek committed
619
620
				? `${LITELLM_API_BASE_URL}/v1`
				: `${OPENAI_API_BASE_URL}`
621
		);
622

Timothy J. Baek's avatar
Timothy J. Baek committed
623
624
625
626
627
		// Wait until history/message have been updated
		await tick();

		scrollToBottom();

628
629
		if (res && res.ok && res.body) {
			const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
630
631

			for await (const update of textStream) {
632
				const { value, done, citations } = update;
Timothy J. Baek's avatar
Timothy J. Baek committed
633
634
635
				if (done || stopResponseFlag || _chatId !== $chatId) {
					responseMessage.done = true;
					messages = messages;
636
637
638
639
640

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

Timothy J. Baek's avatar
Timothy J. Baek committed
641
642
					break;
				}
643

644
645
646
647
648
				if (citations) {
					responseMessage.citations = citations;
					continue;
				}

649
650
651
652
653
				if (responseMessage.content == '' && value == '\n') {
					continue;
				} else {
					responseMessage.content += value;
					messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
654
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
655

Timothy J. Baek's avatar
Timothy J. Baek committed
656
657
658
				if ($settings.notificationEnabled && !document.hasFocus()) {
					const notification = new Notification(`OpenAI ${model}`, {
						body: responseMessage.content,
659
						icon: `${WEBUI_BASE_URL}/static/favicon.png`
Timothy J. Baek's avatar
Timothy J. Baek committed
660
					});
Timothy J. Baek's avatar
Timothy J. Baek committed
661
662
				}

Timothy J. Baek's avatar
Timothy J. Baek committed
663
664
665
				if ($settings.responseAutoCopy) {
					copyToClipboard(responseMessage.content);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
666

Timothy J. Baek's avatar
Timothy J. Baek committed
667
				if ($settings.responseAutoPlayback) {
Timothy J. Baek's avatar
Timothy J. Baek committed
668
					await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
669
670
671
					document.getElementById(`speak-button-${responseMessage.id}`)?.click();
				}

672
				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
673
					scrollToBottom();
674
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
675
			}
676

Timothy J. Baek's avatar
Timothy J. Baek committed
677
			if ($chatId == _chatId) {
678
679
680
681
682
683
684
				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
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
			}
		} 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;
					}
701
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
702
			} else {
Ased Mammad's avatar
Ased Mammad committed
703
				toast.error(
704
705
706
					$i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
						provider: model.name ?? model.id
					})
Ased Mammad's avatar
Ased Mammad committed
707
708
				);
				responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
709
					provider: model.name ?? model.id
Ased Mammad's avatar
Ased Mammad committed
710
				});
711
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
712
713

			responseMessage.error = true;
Ased Mammad's avatar
Ased Mammad committed
714
			responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
715
				provider: model.name ?? model.id
Ased Mammad's avatar
Ased Mammad committed
716
			});
Timothy J. Baek's avatar
Timothy J. Baek committed
717
718
719
720
721
722
723
724
			responseMessage.done = true;
			messages = messages;
		}

		stopResponseFlag = false;
		await tick();

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

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

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

736
737
738
739
740
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

741
	const regenerateResponse = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
742
		console.log('regenerateResponse');
743
744
745
		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
746

747
748
			let userMessage = messages.at(-1);
			let userPrompt = userMessage.content;
749

Timothy J. Baek's avatar
Timothy J. Baek committed
750
			await sendPrompt(userPrompt, userMessage.id);
Timothy J. Baek's avatar
Timothy J. Baek committed
751
		}
752
	};
753

Timothy J. Baek's avatar
Timothy J. Baek committed
754
755
756
757
758
759
	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];
760
761
762
			responseMessage.done = false;
			await tick();

Timothy J. Baek's avatar
Timothy J. Baek committed
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
			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
780
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
781
		} else {
Ased Mammad's avatar
Ased Mammad committed
782
			toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
Timothy J. Baek's avatar
Timothy J. Baek committed
783
784
785
		}
	};

786
787
788
789
790
791
792
793
794
795
796
	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
797
798
			const title = await generateTitle(
				localStorage.token,
799
				$settings?.title?.prompt ??
Ased Mammad's avatar
Ased Mammad committed
800
801
802
					$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}}',
803
804
805
				titleModelId,
				userPrompt,
				titleModel?.external ?? false
806
					? titleModel?.source?.toLowerCase() === 'litellm'
807
808
809
						? `${LITELLM_API_BASE_URL}/v1`
						: `${OPENAI_API_BASE_URL}`
					: `${OLLAMA_API_BASE_URL}/v1`
Timothy J. Baek's avatar
Timothy J. Baek committed
810
811
			);

812
			return title;
813
		} else {
814
			return `${userPrompt}`;
815
816
817
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
818
819
820
821
822
823
824
825
826
827
828
	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));
		}
	};

829
830
831
832
833
834
835
836
837
	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
838
839
840
841

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

		_tags.set(await getAllChatTags(localStorage.token));
844
845
846
847
848
	};

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

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

		_tags.set(await getAllChatTags(localStorage.token));
855
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
856
857
</script>

858
859
<svelte:head>
	<title>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
860
861
862
		{title
			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
			: `${$WEBUI_NAME}`}
863
864
865
	</title>
</svelte:head>

Timothy J. Baek's avatar
Timothy J. Baek committed
866
867
868
869
870
<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
871
872
873
874
875
	<Navbar
		{title}
		bind:selectedModels
		bind:showModelSelector
		shareEnabled={messages.length > 0}
Timothy J. Baek's avatar
Timothy J. Baek committed
876
		{chat}
Timothy J. Baek's avatar
Timothy J. Baek committed
877
878
		{initNewChat}
	/>
Timothy J. Baek's avatar
Timothy J. Baek committed
879
880
	<div class="flex flex-col flex-auto">
		<div
Timothy J. Baek's avatar
Timothy J. Baek committed
881
			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
882
			id="messages-container"
883
			bind:this={messagesContainerElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
884
			on:scroll={(e) => {
885
886
				autoScroll =
					messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
887
					messagesContainerElement.clientHeight + 5;
Timothy J. Baek's avatar
Timothy J. Baek committed
888
889
			}}
		>
Timothy J. Baek's avatar
Timothy J. Baek committed
890
			<div class=" h-full w-full flex flex-col pt-2 pb-4">
Timothy J. Baek's avatar
Timothy J. Baek committed
891
892
893
894
895
896
897
898
				<Messages
					chatId={$chatId}
					{selectedModels}
					{selectedModelfiles}
					{processing}
					bind:history
					bind:messages
					bind:autoScroll
Timothy J. Baek's avatar
Timothy J. Baek committed
899
					bind:prompt
Timothy J. Baek's avatar
Timothy J. Baek committed
900
					bottomPadding={files.length > 0}
Timothy J. Baek's avatar
Timothy J. Baek committed
901
902
					suggestionPrompts={selectedModelfile?.suggestionPrompts ??
						$config.default_prompt_suggestions}
Timothy J. Baek's avatar
Timothy J. Baek committed
903
904
905
906
907
					{sendPrompt}
					{continueGeneration}
					{regenerateResponse}
				/>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
908
		</div>
909
910
	</div>
</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
911
912
913
914
915
916
917
918
919
920

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