+page.svelte 25.7 KB
Newer Older
Timothy J. Baek's avatar
Timothy J. Baek committed
1
<script lang="ts">
2
	import { v4 as uuidv4 } from 'uuid';
Jannik Streidl's avatar
Jannik Streidl committed
3
	import { toast } from 'svelte-sonner';
Timothy J. Baek's avatar
Timothy J. Baek committed
4

Ased Mammad's avatar
Ased Mammad committed
5
	import { onMount, tick, getContext } from 'svelte';
6
	import { goto } from '$app/navigation';
7
	import { page } from '$app/stores';
Timothy J. Baek's avatar
Timothy J. Baek committed
8

Timothy J. Baek's avatar
Timothy J. Baek committed
9
10
11
12
13
14
15
16
	import {
		models,
		modelfiles,
		user,
		settings,
		chats,
		chatId,
		config,
17
		WEBUI_NAME,
Timothy J. Baek's avatar
Timothy J. Baek committed
18
19
		tags as _tags,
		showSidebar
Timothy J. Baek's avatar
Timothy J. Baek committed
20
	} from '$lib/stores';
21
	import { copyToClipboard, splitStream } from '$lib/utils';
Timothy J. Baek's avatar
Timothy J. Baek committed
22

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

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

	const i18n = getContext('i18n');

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

177
		if (selectedModels.includes('')) {
Ased Mammad's avatar
Ased Mammad committed
178
			toast.error($i18n.t('Model not selected'));
179
180
181
		} else if (messages.length != 0 && messages.at(-1).done != true) {
			// Response not done
			console.log('wait');
182
183
184
185
186
187
		} else if (
			files.length > 0 &&
			files.filter((file) => file.upload_status === false).length > 0
		) {
			// Upload not done
			toast.error(
Ased Mammad's avatar
Ased Mammad committed
188
189
190
				$i18n.t(
					`Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.`
				)
191
			);
192
193
194
195
196
197
198
199
200
201
202
		} else {
			// Reset chat message textarea height
			document.getElementById('chat-textarea').style.height = '';

			// Create user message
			let userMessageId = uuidv4();
			let userMessage = {
				id: userMessageId,
				parentId: messages.length !== 0 ? messages.at(-1).id : null,
				childrenIds: [],
				role: 'user',
Timothy J. Baek's avatar
Timothy J. Baek committed
203
				user: _user ?? undefined,
204
				content: userPrompt,
Timothy J. Baek's avatar
Timothy J. Baek committed
205
				files: files.length > 0 ? files : undefined,
206
				models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx),
Timothy J. Baek's avatar
Timothy J. Baek committed
207
				timestamp: Math.floor(Date.now() / 1000) // Unix epoch
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
			};

			// Add message to history and Set currentId to messageId
			history.messages[userMessageId] = userMessage;
			history.currentId = userMessageId;

			// Append messageId to childrenIds of parent message
			if (messages.length !== 0) {
				history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
			}

			// Wait until history/message have been updated
			await tick();

			// Create new chat if only one message in messages
			if (messages.length == 1) {
				if ($settings.saveChatHistory ?? true) {
					chat = await createNewChat(localStorage.token, {
						id: $chatId,
227
						title: $i18n.t('New Chat'),
228
229
230
231
232
233
234
						models: selectedModels,
						system: $settings.system ?? undefined,
						options: {
							...($settings.options ?? {})
						},
						messages: messages,
						history: history,
Timothy J. Baek's avatar
Timothy J. Baek committed
235
						tags: [],
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
						timestamp: Date.now()
					});
					await chats.set(await getChatList(localStorage.token));
					await chatId.set(chat.id);
				} else {
					await chatId.set('local');
				}
				await tick();
			}

			// Reset chat input textarea
			prompt = '';
			files = [];

			// Send prompt
			await sendPrompt(userPrompt, userMessageId);
		}
	};

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

Timothy J. Baek's avatar
Timothy J. Baek committed
258
259
260
261
262
263
264
265
266
		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
267
268
269
270
271
272
273
274
				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
275
276
277
278
279

				console.log(userContext);
			}
		}

Timothy J. Baek's avatar
Timothy J. Baek committed
280
		await Promise.all(
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
			(modelId ? [modelId] : atSelectedModel !== '' ? [atSelectedModel.id] : selectedModels).map(
				async (modelId) => {
					console.log('modelId', modelId);
					const model = $models.filter((m) => m.id === modelId).at(0);

					if (model) {
						// Create response message
						let responseMessageId = uuidv4();
						let responseMessage = {
							parentId: parentId,
							id: responseMessageId,
							childrenIds: [],
							role: 'assistant',
							content: '',
							model: model.id,
Timothy J. Baek's avatar
Timothy J. Baek committed
296
							userContext: userContext,
297
298
299
300
301
302
303
304
305
306
307
308
309
310
							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
311

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

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

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

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

		// Scroll down
Timothy J. Baek's avatar
Timothy J. Baek committed
335
		scrollToBottom();
336

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

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

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

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

411
		if (res && res.ok) {
412
413
			console.log('controller', controller);

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

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

					currentRequestId = null;

Rohit Das's avatar
Rohit Das committed
432
433
					break;
				}
434

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

586
587
		console.log(docs);

Timothy J. Baek's avatar
Timothy J. Baek committed
588
		scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
589

590
591
592
593
594
595
596
		try {
			const [res, controller] = await generateOpenAIChatCompletion(
				localStorage.token,
				{
					model: model.id,
					stream: true,
					messages: [
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
597
						$settings.system || (responseMessage?.userContext ?? null)
598
							? {
599
									role: 'system',
Timothy J. Baek's avatar
Timothy J. Baek committed
600
									content:
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
601
										$settings.system + (responseMessage?.userContext ?? null)
Timothy J. Baek's avatar
Timothy J. Baek committed
602
603
											? `\n\nUser Context:\n${responseMessage.userContext.join('\n')}`
											: ''
604
							  }
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
							: 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
658

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

662
			scrollToBottom();
663

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

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

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

681
682
						break;
					}
683

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

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

696
697
698
699
700
701
					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
702

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

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

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

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

		stopResponseFlag = false;
		await tick();

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

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

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

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

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

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

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

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

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

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

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

865
			return title;
866
		} else {
867
			return `${userPrompt}`;
868
869
870
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
871
872
873
874
875
876
877
878
879
880
881
	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));
		}
	};

882
883
884
885
886
887
888
889
890
	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
891
892
893
894

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

		_tags.set(await getAllChatTags(localStorage.token));
897
898
899
900
901
	};

	const deleteTag = async (tagName) => {
		const res = await deleteTagById(localStorage.token, $chatId, tagName);
		tags = await getTags();
Timothy J. Baek's avatar
Timothy J. Baek committed
902
903
904
905

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

		_tags.set(await getAllChatTags(localStorage.token));
908
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
909
910
</script>

911
912
<svelte:head>
	<title>
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
913
914
915
		{title
			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}`
			: `${$WEBUI_NAME}`}
916
917
918
	</title>
</svelte:head>

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

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