+page.svelte 28.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
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, runWebSearch } from '$lib/apis/rag';
34
35
36
37
38
	import {
		generateOpenAIChatCompletion,
		generateSearchQuery,
		generateTitle
	} from '$lib/apis/openai';
39
40
41
42

	import MessageInput from '$lib/components/chat/MessageInput.svelte';
	import Messages from '$lib/components/chat/Messages.svelte';
	import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
43
	import Navbar from '$lib/components/layout/Navbar.svelte';
44
	import { RAGTemplate } from '$lib/utils/rag';
45
	import { LITELLM_API_BASE_URL, OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants';
46
	import { WEBUI_BASE_URL } from '$lib/constants';
47
	import { createOpenAITextStream } from '$lib/apis/streaming';
Timothy J. Baek's avatar
Timothy J. Baek committed
48
	import { queryMemory } from '$lib/apis/memories';
Ased Mammad's avatar
Ased Mammad committed
49
50
51

	const i18n = getContext('i18n');

52
53
	let stopResponseFlag = false;
	let autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
54
	let processing = '';
55
	let messagesContainerElement: HTMLDivElement;
56
57
	let currentRequestId = null;

58
	let showModelSelector = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
59

Timothy J. Baek's avatar
Timothy J. Baek committed
60
	let selectedModels = [''];
Timothy J. Baek's avatar
Timothy J. Baek committed
61
	let atSelectedModel = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
62

63
64
	let useWebSearch = false;

65
66
67
68
69
70
	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;
71

72
73
74
75
76
77
78
79
80
81
82
	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 })
		};
	}, {});

83
	let chat = null;
84
	let tags = [];
85

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

109
	onMount(async () => {
110
		await initNewChat();
111
112
113
114
115
116
	});

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

117
	const initNewChat = async () => {
118
		if (currentRequestId !== null) {
Timothy J. Baek's avatar
Timothy J. Baek committed
119
			await cancelOllamaRequest(localStorage.token, currentRequestId);
120
121
			currentRequestId = null;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
122
		window.history.replaceState(history.state, '', `/`);
123
		await chatId.set('');
Timothy J. Baek's avatar
Timothy J. Baek committed
124

125
		autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
126

127
128
129
130
131
		title = '';
		messages = [];
		history = {
			messages: {},
			currentId: null
Timothy J. Baek's avatar
Timothy J. Baek committed
132
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
133
134
135
136
137
138
139
140
141
142

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

144
145
146
147
148
149
150
151
		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
152
153
154
155
		selectedModels = selectedModels.map((modelId) =>
			$models.map((m) => m.id).includes(modelId) ? modelId : ''
		);

156
157
158
159
		let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
		settings.set({
			..._settings
		});
160
161
162

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

Timothy J. Baek's avatar
Timothy J. Baek committed
165
166
	const scrollToBottom = async () => {
		await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
167
168
169
		if (messagesContainerElement) {
			messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
		}
Timothy J. Baek's avatar
Timothy J. Baek committed
170
171
	};

172
173
174
175
	//////////////////////////
	// Ollama functions
	//////////////////////////

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

Timothy J. Baek's avatar
Timothy J. Baek committed
179
180
181
182
		selectedModels = selectedModels.map((modelId) =>
			$models.map((m) => m.id).includes(modelId) ? modelId : ''
		);

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

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

261
	const sendPrompt = async (prompt, parentId, modelId = null) => {
262
		const _chatId = JSON.parse(JSON.stringify($chatId));
263

Timothy J. Baek's avatar
Timothy J. Baek committed
264
		await Promise.all(
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
			(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
refac  
Timothy J. Baek committed
280
							userContext: null,
281
282
283
284
285
286
287
288
289
290
291
292
293
294
							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
295

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
296
297
						await tick();

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
298
						let userContext = null;
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
						if ($settings?.memory ?? false) {
							if (userContext === null) {
								const res = await queryMemory(localStorage.token, prompt).catch((error) => {
									toast.error(error);
									return null;
								});

								if (res) {
									if (res.documents[0].length > 0) {
										userContext = res.documents.reduce((acc, doc, index) => {
											const createdAtTimestamp = res.metadatas[index][0].created_at;
											const createdAtDate = new Date(createdAtTimestamp * 1000)
												.toISOString()
												.split('T')[0];
											acc.push(`${index + 1}. [${createdAtDate}]. ${doc[0]}`);
											return acc;
										}, []);
									}

									console.log(userContext);
								}
							}
						}
						responseMessage.userContext = userContext;

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

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

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

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

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

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

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

388
		const messagesBody = [
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
389
			$settings.system || (responseMessage?.userContext ?? null)
390
391
				? {
						role: 'system',
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
392
393
						content: `${$settings?.system ?? ''}${
							responseMessage?.userContext ?? null
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
394
								? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
Timothy J. Baek's avatar
Timothy J. Baek committed
395
								: ''
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
396
						}`
397
398
				  }
				: undefined,
Danny Liu's avatar
Danny Liu committed
399
			...messages
400
401
		]
			.filter((message) => message)
Timothy J. Baek's avatar
Timothy J. Baek committed
402
403
404
405
406
407
408
409
410
411
412
413
414
			.map((message, idx, arr) => {
				// Prepare the base message object
				const baseMessage = {
					role: message.role,
					content: arr.length - 2 !== idx ? message.content : message?.raContent ?? message.content
				};

				// Extract and format image URLs if any exist
				const imageUrls = message.files
					?.filter((file) => file.type === 'image')
					.map((file) => file.url.slice(file.url.indexOf(',') + 1));

				// Add images array only if it contains elements
415
				if (imageUrls && imageUrls.length > 0 && message.role === 'user') {
Timothy J. Baek's avatar
Timothy J. Baek committed
416
417
418
419
420
					baseMessage.images = imageUrls;
				}

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

		let lastImageIndex = -1;

		// Find the index of the last object with images
		messagesBody.forEach((item, index) => {
			if (item.images) {
				lastImageIndex = index;
			}
		});

		// Remove images from all but the last one
		messagesBody.forEach((item, index) => {
			if (index !== lastImageIndex) {
				delete item.images;
			}
		});

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

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

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

Rohit Das's avatar
Rohit Das committed
466
467
468
469
470
471
472
473
474
475
			const reader = res.body
				.pipeThrough(new TextDecoderStream())
				.pipeThrough(splitStream('\n'))
				.getReader();

			while (true) {
				const { value, done } = await reader.read();
				if (done || stopResponseFlag || _chatId !== $chatId) {
					responseMessage.done = true;
					messages = messages;
476
477
478

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

					currentRequestId = null;

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

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

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

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

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

504
505
506
507
508
509
510
511
512
513
514
							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
515
								} else {
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
									responseMessage.done = true;

									if (responseMessage.content == '') {
										responseMessage.error = true;
										responseMessage.content =
											'Oops! No text generated from Ollama, Please try again.';
									}

									responseMessage.context = data.context ?? null;
									responseMessage.info = {
										total_duration: data.total_duration,
										load_duration: data.load_duration,
										sample_count: data.sample_count,
										sample_duration: data.sample_duration,
										prompt_eval_count: data.prompt_eval_count,
										prompt_eval_duration: data.prompt_eval_duration,
										eval_count: data.eval_count,
										eval_duration: data.eval_duration
									};
Rohit Das's avatar
Rohit Das committed
535
									messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
536

537
538
539
540
541
542
543
									if ($settings.notificationEnabled && !document.hasFocus()) {
										const notification = new Notification(
											selectedModelfile
												? `${
														selectedModelfile.title.charAt(0).toUpperCase() +
														selectedModelfile.title.slice(1)
												  }`
Timothy J. Baek's avatar
Timothy J. Baek committed
544
												: `${model}`,
545
546
											{
												body: responseMessage.content,
547
												icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
548
549
550
551
552
553
554
											}
										);
									}

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

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

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

577
			if ($chatId == _chatId) {
578
579
580
581
582
583
584
				if ($settings.saveChatHistory ?? true) {
					chat = await updateChatById(localStorage.token, _chatId, {
						messages: messages,
						history: history
					});
					await chats.set(await getChatList(localStorage.token));
				}
585
			}
586
587
588
		} else {
			if (res !== null) {
				const error = await res.json();
589
				console.log(error);
590
591
				if ('detail' in error) {
					toast.error(error.detail);
592
					responseMessage.content = error.detail;
593
594
				} else {
					toast.error(error.error);
595
					responseMessage.content = error.error;
596
				}
597
			} else {
Ased Mammad's avatar
Ased Mammad committed
598
599
600
601
602
603
				toast.error(
					$i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { provider: 'Ollama' })
				);
				responseMessage.content = $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, {
					provider: 'Ollama'
				});
604
605
			}

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

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

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

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

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

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

638
639
		console.log(docs);

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

642
643
644
645
646
647
648
		try {
			const [res, controller] = await generateOpenAIChatCompletion(
				localStorage.token,
				{
					model: model.id,
					stream: true,
					messages: [
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
649
						$settings.system || (responseMessage?.userContext ?? null)
650
							? {
651
									role: 'system',
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
652
653
									content: `${$settings?.system ?? ''}${
										responseMessage?.userContext ?? null
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
654
											? `\n\nUser Context:\n${(responseMessage?.userContext ?? []).join('\n')}`
Timothy J. Baek's avatar
Timothy J. Baek committed
655
											: ''
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
656
									}`
657
							  }
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
							: 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
711

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

715
			scrollToBottom();
716

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

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

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

734
735
						break;
					}
736

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

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

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

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

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

765
766
767
					if (autoScroll) {
						scrollToBottom();
					}
768
				}
769
770
771
772
773
774
775
776

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

		stopResponseFlag = false;
		await tick();

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

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

797
798
			const _title = await generateChatTitle(userPrompt);
			await setChatTitle(_chatId, _title);
799
		}
800
801
	};

802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
	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;
	};

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

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

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

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

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

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

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

918
			return title;
919
		} else {
920
			return `${userPrompt}`;
921
922
923
		}
	};

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

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

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

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
953
954
955
956
957
958
959
960
961
962
963
	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));
		}
	};

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

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

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

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

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

		_tags.set(await getAllChatTags(localStorage.token));
990
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
991
992
</script>

993
994
<svelte:head>
	<title>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
995
996
997
		{title
			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
			: `${$WEBUI_NAME}`}
998
999
1000
	</title>
</svelte:head>

Timothy J. Baek's avatar
Timothy J. Baek committed
1001
1002
<div
	class="min-h-screen max-h-screen {$showSidebar
Timothy J. Baek's avatar
Timothy J. Baek committed
1003
		? 'md:max-w-[calc(100%-260px)]'
Timothy J. Baek's avatar
Timothy J. Baek committed
1004
1005
		: ''} w-full max-w-full flex flex-col"
>
Timothy J. Baek's avatar
Timothy J. Baek committed
1006
1007
1008
1009
1010
	<Navbar
		{title}
		bind:selectedModels
		bind:showModelSelector
		shareEnabled={messages.length > 0}
Timothy J. Baek's avatar
Timothy J. Baek committed
1011
		{chat}
Timothy J. Baek's avatar
Timothy J. Baek committed
1012
1013
		{initNewChat}
	/>
Timothy J. Baek's avatar
Timothy J. Baek committed
1014
1015
	<div class="flex flex-col flex-auto">
		<div
Timothy J. Baek's avatar
Timothy J. Baek committed
1016
			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
1017
			id="messages-container"
1018
			bind:this={messagesContainerElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
1019
			on:scroll={(e) => {
1020
1021
				autoScroll =
					messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
1022
					messagesContainerElement.clientHeight + 5;
Timothy J. Baek's avatar
Timothy J. Baek committed
1023
1024
			}}
		>
Timothy J. Baek's avatar
Timothy J. Baek committed
1025
			<div class=" h-full w-full flex flex-col pt-2 pb-4">
Timothy J. Baek's avatar
Timothy J. Baek committed
1026
1027
1028
1029
1030
1031
1032
1033
				<Messages
					chatId={$chatId}
					{selectedModels}
					{selectedModelfiles}
					{processing}
					bind:history
					bind:messages
					bind:autoScroll
Timothy J. Baek's avatar
Timothy J. Baek committed
1034
					bind:prompt
Timothy J. Baek's avatar
Timothy J. Baek committed
1035
					bottomPadding={files.length > 0}
Timothy J. Baek's avatar
Timothy J. Baek committed
1036
1037
					suggestionPrompts={selectedModelfile?.suggestionPrompts ??
						$config.default_prompt_suggestions}
Timothy J. Baek's avatar
Timothy J. Baek committed
1038
1039
1040
1041
1042
					{sendPrompt}
					{continueGeneration}
					{regenerateResponse}
				/>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
1043
		</div>
1044
1045
	</div>
</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
1046
1047
1048
1049
1050
1051

<MessageInput
	bind:files
	bind:prompt
	bind:autoScroll
	bind:selectedModel={atSelectedModel}
1052
	bind:useWebSearch
Timothy J. Baek's avatar
Timothy J. Baek committed
1053
1054
1055
	{messages}
	{submitPrompt}
	{stopResponse}
1056
	webSearchAvailable={$config.websearch ?? false}
Timothy J. Baek's avatar
Timothy J. Baek committed
1057
/>