+page.svelte 27.4 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';
Timothy J. Baek's avatar
Timothy J. Baek committed
45
	import { queryMemory } from '$lib/apis/memories';
Ased Mammad's avatar
Ased Mammad committed
46
47
48

	const i18n = getContext('i18n');

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
57
	let selectedModels = [''];
58
	let atSelectedModel: Model | undefined;
Timothy J. Baek's avatar
Timothy J. Baek committed
59

60
61
62
63
64
65
	let selectedModelfile = null;
	$: selectedModelfile =
		selectedModels.length === 1 &&
		$modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0]).length > 0
			? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
			: null;
66

67
68
69
70
71
72
73
74
75
76
77
	let selectedModelfiles = {};
	$: selectedModelfiles = selectedModels.reduce((a, tagName, i, arr) => {
		const modelfile =
			$modelfiles.filter((modelfile) => modelfile.tagName === tagName)?.at(0) ?? undefined;

		return {
			...a,
			...(modelfile && { [tagName]: modelfile })
		};
	}, {});

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

Timothy J. Baek's avatar
Timothy J. Baek committed
81
	let title = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
82
	let prompt = '';
83
	let files = [];
84
	let messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
	let history = {
		messages: {},
		currentId: null
	};

	$: if (history.currentId !== null) {
		let _messages = [];

		let currentMessage = history.messages[history.currentId];
		while (currentMessage !== null) {
			_messages.unshift({ ...currentMessage });
			currentMessage =
				currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null;
		}
		messages = _messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
100
101
	} else {
		messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
102
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
103

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

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

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

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

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

		if ($page.url.searchParams.get('models')) {
			selectedModels = $page.url.searchParams.get('models')?.split(',');
		} else if ($settings?.models) {
			selectedModels = $settings?.models;
		} else if ($config?.default_models) {
			selectedModels = $config?.default_models.split(',');
		} else {
			selectedModels = [''];
		}
138

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

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

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

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

167
168
169
170
	//////////////////////////
	// Ollama functions
	//////////////////////////

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

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

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

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
259
260
261
262
263
264
265
266
267
		let userContext = null;

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

			if (res) {
Timothy J. Baek's avatar
Timothy J. Baek committed
268
269
270
271
272
273
274
275
				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;
					}, []);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
276
277
278
279
280

				console.log(userContext);
			}
		}

Timothy J. Baek's avatar
Timothy J. Baek committed
281
		await Promise.all(
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
			(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
							})
301
						);
302
303
304
305
306
307
308
309
310
						// Create response message
						let responseMessageId = uuidv4();
						let responseMessage = {
							parentId: parentId,
							id: responseMessageId,
							childrenIds: [],
							role: 'assistant',
							content: '',
							model: model.id,
Timothy J. Baek's avatar
Timothy J. Baek committed
311
							userContext: userContext,
312
313
314
315
316
317
318
319
320
321
322
323
324
325
							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
326

327
328
329
330
331
332
333
						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 }));
334
					}
335

336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
					// 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
359
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
360

361
362
363
364
					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
365
					}
366
367
				} else {
					toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
Timothy J. Baek's avatar
Timothy J. Baek committed
368
				}
369
			})
Timothy J. Baek's avatar
Timothy J. Baek committed
370
		);
371

Timothy J. Baek's avatar
Timothy J. Baek committed
372
		await chats.set(await getChatList(localStorage.token));
373
374
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
375
	const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
376
		const modelName = model.custom_info?.name ?? model.name ?? model.id;
377
		model = model.id;
Timothy J. Baek's avatar
Timothy J. Baek committed
378
		const responseMessage = history.messages[responseMessageId];
Timothy J. Baek's avatar
Timothy J. Baek committed
379

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

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
384
		scrollToBottom();
385

386
		const messagesBody = [
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
387
			$settings.system || (responseMessage?.userContext ?? null)
388
389
				? {
						role: 'system',
Timothy J. Baek's avatar
Timothy J. Baek committed
390
						content:
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
391
							$settings.system + (responseMessage?.userContext ?? null)
Timothy J. Baek's avatar
Timothy J. Baek committed
392
393
								? `\n\nUser Context:\n${responseMessage.userContext.join('\n')}`
								: ''
394
395
				  }
				: undefined,
Danny Liu's avatar
Danny Liu committed
396
			...messages
397
398
		]
			.filter((message) => message)
Timothy J. Baek's avatar
Timothy J. Baek committed
399
400
401
402
403
404
405
406
407
408
409
410
411
			.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
412
				if (imageUrls && imageUrls.length > 0 && message.role === 'user') {
Timothy J. Baek's avatar
Timothy J. Baek committed
413
414
415
416
417
					baseMessage.images = imageUrls;
				}

				return baseMessage;
			});
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434

		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
435
436
437
438
439
440
441
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

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

460
		if (res && res.ok) {
461
462
			console.log('controller', controller);

Rohit Das's avatar
Rohit Das committed
463
464
465
466
467
468
469
470
471
472
			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;
473
474
475

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

					currentRequestId = null;

Rohit Das's avatar
Rohit Das committed
481
482
					break;
				}
483

Rohit Das's avatar
Rohit Das committed
484
485
				try {
					let lines = value.split('\n');
486

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

492
493
494
495
496
							if ('citations' in data) {
								responseMessage.citations = data.citations;
								continue;
							}

Rohit Das's avatar
Rohit Das committed
497
498
499
							if ('detail' in data) {
								throw data;
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
500

501
502
503
504
505
506
507
508
509
510
511
							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
512
								} else {
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
									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
532
									messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
533

534
535
536
537
538
539
540
									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
541
												: `${model}`,
542
543
											{
												body: responseMessage.content,
544
												icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
545
546
547
548
549
550
551
											}
										);
									}

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

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

				if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
570
					scrollToBottom();
571
				}
572
			}
573

574
			if ($chatId == _chatId) {
575
576
577
578
579
580
581
				if ($settings.saveChatHistory ?? true) {
					chat = await updateChatById(localStorage.token, _chatId, {
						messages: messages,
						history: history
					});
					await chats.set(await getChatList(localStorage.token));
				}
582
			}
583
584
585
		} else {
			if (res !== null) {
				const error = await res.json();
586
				console.log(error);
587
588
				if ('detail' in error) {
					toast.error(error.detail);
589
					responseMessage.content = error.detail;
590
591
				} else {
					toast.error(error.error);
592
					responseMessage.content = error.error;
593
				}
594
			} else {
Ased Mammad's avatar
Ased Mammad committed
595
596
597
598
599
600
				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'
				});
601
602
			}

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

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

614
		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
615
			scrollToBottom();
616
		}
617

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
628
629
630
631
632
633
634
		const docs = messages
			.filter((message) => message?.files ?? null)
			.map((message) =>
				message.files.filter((item) => item.type === 'doc' || item.type === 'collection')
			)
			.flat(1);

635
636
		console.log(docs);

Timothy J. Baek's avatar
Timothy J. Baek committed
637
		scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
638

639
640
641
642
643
644
645
		try {
			const [res, controller] = await generateOpenAIChatCompletion(
				localStorage.token,
				{
					model: model.id,
					stream: true,
					messages: [
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
646
						$settings.system || (responseMessage?.userContext ?? null)
647
							? {
648
									role: 'system',
Timothy J. Baek's avatar
Timothy J. Baek committed
649
									content:
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
650
										$settings.system + (responseMessage?.userContext ?? null)
Timothy J. Baek's avatar
Timothy J. Baek committed
651
652
											? `\n\nUser Context:\n${responseMessage.userContext.join('\n')}`
											: ''
653
							  }
654
655
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
							: 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
707

708
709
			// Wait until history/message have been updated
			await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
710

711
			scrollToBottom();
712

713
714
			if (res && res.ok && res.body) {
				const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
715

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

726
727
728
						if (stopResponseFlag) {
							controller.abort('User: Stop Response');
						}
729

730
731
						break;
					}
732

733
734
735
736
					if (citations) {
						responseMessage.citations = citations;
						continue;
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
737

738
739
740
741
742
743
					if (responseMessage.content == '' && value == '\n') {
						continue;
					} else {
						responseMessage.content += value;
						messages = messages;
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
744

745
746
747
748
749
750
					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
751

752
753
754
					if ($settings.responseAutoCopy) {
						copyToClipboard(responseMessage.content);
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
755

756
757
758
759
					if ($settings.responseAutoPlayback) {
						await tick();
						document.getElementById(`speak-button-${responseMessage.id}`)?.click();
					}
760

761
762
763
					if (autoScroll) {
						scrollToBottom();
					}
764
				}
765
766
767
768
769
770
771
772

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

		stopResponseFlag = false;
		await tick();

		if (autoScroll) {
Timothy J. Baek's avatar
Timothy J. Baek committed
787
			scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
788
789
790
791
		}

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

793
794
			const _title = await generateChatTitle(userPrompt);
			await setChatTitle(_chatId, _title);
795
		}
796
797
	};

798
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
	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}}.`, {
827
				provider: model.custom_info?.name ?? model.name ?? model.id
828
829
830
831
832
833
834
835
			}) +
			'\n' +
			errorMessage;
		responseMessage.done = true;

		messages = messages;
	};

836
837
838
839
840
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
841
	const regenerateResponse = async (message) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
842
		console.log('regenerateResponse');
Timothy J. Baek's avatar
Timothy J. Baek committed
843

Timothy J. Baek's avatar
Timothy J. Baek committed
844
845
		if (messages.length != 0) {
			let userMessage = history.messages[message.parentId];
846
			let userPrompt = userMessage.content;
847

Timothy J. Baek's avatar
Timothy J. Baek committed
848
849
850
851
852
			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
853
		}
854
	};
855

Timothy J. Baek's avatar
Timothy J. Baek committed
856
857
858
859
860
861
	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];
862
863
864
			responseMessage.done = false;
			await tick();

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

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

914
			return title;
915
		} else {
916
			return `${userPrompt}`;
917
918
919
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
920
921
922
923
924
925
926
927
928
929
930
	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));
		}
	};

931
932
933
934
935
936
937
938
939
	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
940
941
942
943

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

		_tags.set(await getAllChatTags(localStorage.token));
946
947
948
949
950
	};

	const deleteTag = async (tagName) => {
		const res = await deleteTagById(localStorage.token, $chatId, tagName);
		tags = await getTags();
Timothy J. Baek's avatar
Timothy J. Baek committed
951
952
953
954

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

		_tags.set(await getAllChatTags(localStorage.token));
957
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
958
959
</script>

960
961
<svelte:head>
	<title>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
962
963
964
		{title
			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
			: `${$WEBUI_NAME}`}
965
966
967
	</title>
</svelte:head>

Timothy J. Baek's avatar
Timothy J. Baek committed
968
969
<div
	class="min-h-screen max-h-screen {$showSidebar
Timothy J. Baek's avatar
Timothy J. Baek committed
970
		? 'md:max-w-[calc(100%-260px)]'
Timothy J. Baek's avatar
Timothy J. Baek committed
971
972
		: ''} w-full max-w-full flex flex-col"
>
Timothy J. Baek's avatar
Timothy J. Baek committed
973
974
975
976
977
	<Navbar
		{title}
		bind:selectedModels
		bind:showModelSelector
		shareEnabled={messages.length > 0}
Timothy J. Baek's avatar
Timothy J. Baek committed
978
		{chat}
Timothy J. Baek's avatar
Timothy J. Baek committed
979
980
		{initNewChat}
	/>
Timothy J. Baek's avatar
Timothy J. Baek committed
981
982
	<div class="flex flex-col flex-auto">
		<div
Timothy J. Baek's avatar
Timothy J. Baek committed
983
			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
984
			id="messages-container"
985
			bind:this={messagesContainerElement}
Timothy J. Baek's avatar
Timothy J. Baek committed
986
			on:scroll={(e) => {
987
988
				autoScroll =
					messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
989
					messagesContainerElement.clientHeight + 5;
Timothy J. Baek's avatar
Timothy J. Baek committed
990
991
			}}
		>
Timothy J. Baek's avatar
Timothy J. Baek committed
992
			<div class=" h-full w-full flex flex-col pt-2 pb-4">
Timothy J. Baek's avatar
Timothy J. Baek committed
993
994
995
996
997
998
999
1000
				<Messages
					chatId={$chatId}
					{selectedModels}
					{selectedModelfiles}
					{processing}
					bind:history
					bind:messages
					bind:autoScroll
Timothy J. Baek's avatar
Timothy J. Baek committed
1001
					bind:prompt
Timothy J. Baek's avatar
Timothy J. Baek committed
1002
					bottomPadding={files.length > 0}
Timothy J. Baek's avatar
Timothy J. Baek committed
1003
1004
					suggestionPrompts={selectedModelfile?.suggestionPrompts ??
						$config.default_prompt_suggestions}
Timothy J. Baek's avatar
Timothy J. Baek committed
1005
1006
1007
1008
1009
					{sendPrompt}
					{continueGeneration}
					{regenerateResponse}
				/>
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
1010
		</div>
1011
1012
	</div>
</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
1013
1014
1015
1016
1017
1018

<MessageInput
	bind:files
	bind:prompt
	bind:autoScroll
	bind:selectedModel={atSelectedModel}
1019
	{selectedModels}
Timothy J. Baek's avatar
Timothy J. Baek committed
1020
1021
1022
1023
	{messages}
	{submitPrompt}
	{stopResponse}
/>