Messages.svelte 11.4 KB
Newer Older
1
2
<script lang="ts">
	import { v4 as uuidv4 } from 'uuid';
3
	import { chats, config, settings, user as _user, mobile } from '$lib/stores';
4
	import { tick, getContext, onMount } from 'svelte';
5

Jannik Streidl's avatar
Jannik Streidl committed
6
	import { toast } from 'svelte-sonner';
Timothy J. Baek's avatar
Timothy J. Baek committed
7
	import { getChatList, updateChatById } from '$lib/apis/chats';
8

9
10
11
	import UserMessage from './Messages/UserMessage.svelte';
	import ResponseMessage from './Messages/ResponseMessage.svelte';
	import Placeholder from './Messages/Placeholder.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
12
	import Spinner from '../common/Spinner.svelte';
13
	import { imageGenerations } from '$lib/apis/images';
14
	import { copyToClipboard, findWordIndices } from '$lib/utils';
15
16
	import CompareMessages from './Messages/CompareMessages.svelte';
	import { stringify } from 'postcss';
17

18
19
	const i18n = getContext('i18n');

20
	export let chatId = '';
21
	export let readOnly = false;
22
	export let sendPrompt: Function;
Timothy J. Baek's avatar
Timothy J. Baek committed
23
	export let continueGeneration: Function;
24
	export let regenerateResponse: Function;
25
	export let chatActionHandler: Function;
26

27
	export let user = $_user;
Timothy J. Baek's avatar
Timothy J. Baek committed
28
	export let prompt;
Timothy J. Baek's avatar
Timothy J. Baek committed
29
	export let processing = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
30
	export let bottomPadding = false;
31
32
33
34
	export let autoScroll;
	export let history = {};
	export let messages = [];

35
	export let selectedModels;
36

Timothy J. Baek's avatar
Timothy J. Baek committed
37
38
39
	$: if (autoScroll && bottomPadding) {
		(async () => {
			await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
40
			scrollToBottom();
Timothy J. Baek's avatar
Timothy J. Baek committed
41
42
43
		})();
	}

Timothy J. Baek's avatar
Timothy J. Baek committed
44
45
46
47
48
	const scrollToBottom = () => {
		const element = document.getElementById('messages-container');
		element.scrollTop = element.scrollHeight;
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
49
50
51
52
	const copyToClipboardWithToast = async (text) => {
		const res = await copyToClipboard(text);
		if (res) {
			toast.success($i18n.t('Copying to clipboard was successful!'));
53
54
55
		}
	};

56
57
	const confirmEditMessage = async (messageId, content) => {
		let userPrompt = content;
58
59
60
61
62
63
64
		let userMessageId = uuidv4();

		let userMessage = {
			id: userMessageId,
			parentId: history.messages[messageId].parentId,
			childrenIds: [],
			role: 'user',
Timothy J. Baek's avatar
Timothy J. Baek committed
65
			content: userPrompt,
66
67
			...(history.messages[messageId].files && { files: history.messages[messageId].files }),
			models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
		};

		let messageParentId = history.messages[messageId].parentId;

		if (messageParentId !== null) {
			history.messages[messageParentId].childrenIds = [
				...history.messages[messageParentId].childrenIds,
				userMessageId
			];
		}

		history.messages[userMessageId] = userMessage;
		history.currentId = userMessageId;

		await tick();
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
83
		await sendPrompt(userPrompt, userMessageId);
84
85
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
86
	const updateChatMessages = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
87
88
89
90
91
92
93
		await tick();
		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
94
95
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
96
97
98
99
100
101
102
	const confirmEditResponseMessage = async (messageId, content) => {
		history.messages[messageId].originalContent = history.messages[messageId].content;
		history.messages[messageId].content = content;

		await updateChatMessages();
	};

103
	const rateMessage = async (messageId, rating) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
104
105
106
107
		history.messages[messageId].annotation = {
			...history.messages[messageId].annotation,
			rating: rating
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
108

Timothy J. Baek's avatar
Timothy J. Baek committed
109
		await updateChatMessages();
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
	};

	const showPreviousMessage = async (message) => {
		if (message.parentId !== null) {
			let messageId =
				history.messages[message.parentId].childrenIds[
					Math.max(history.messages[message.parentId].childrenIds.indexOf(message.id) - 1, 0)
				];

			if (message.id !== messageId) {
				let messageChildrenIds = history.messages[messageId].childrenIds;

				while (messageChildrenIds.length !== 0) {
					messageId = messageChildrenIds.at(-1);
					messageChildrenIds = history.messages[messageId].childrenIds;
				}

				history.currentId = messageId;
			}
		} else {
			let childrenIds = Object.values(history.messages)
				.filter((message) => message.parentId === null)
				.map((message) => message.id);
			let messageId = childrenIds[Math.max(childrenIds.indexOf(message.id) - 1, 0)];

			if (message.id !== messageId) {
				let messageChildrenIds = history.messages[messageId].childrenIds;

				while (messageChildrenIds.length !== 0) {
					messageId = messageChildrenIds.at(-1);
					messageChildrenIds = history.messages[messageId].childrenIds;
				}

				history.currentId = messageId;
			}
		}

		await tick();

Timothy J. Baek's avatar
Timothy J. Baek committed
149
		const element = document.getElementById('messages-container');
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
150
		autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
151
152

		setTimeout(() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
153
			scrollToBottom();
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
		}, 100);
	};

	const showNextMessage = async (message) => {
		if (message.parentId !== null) {
			let messageId =
				history.messages[message.parentId].childrenIds[
					Math.min(
						history.messages[message.parentId].childrenIds.indexOf(message.id) + 1,
						history.messages[message.parentId].childrenIds.length - 1
					)
				];

			if (message.id !== messageId) {
				let messageChildrenIds = history.messages[messageId].childrenIds;

				while (messageChildrenIds.length !== 0) {
					messageId = messageChildrenIds.at(-1);
					messageChildrenIds = history.messages[messageId].childrenIds;
				}

				history.currentId = messageId;
			}
		} else {
			let childrenIds = Object.values(history.messages)
				.filter((message) => message.parentId === null)
				.map((message) => message.id);
			let messageId =
				childrenIds[Math.min(childrenIds.indexOf(message.id) + 1, childrenIds.length - 1)];

			if (message.id !== messageId) {
				let messageChildrenIds = history.messages[messageId].childrenIds;

				while (messageChildrenIds.length !== 0) {
					messageId = messageChildrenIds.at(-1);
					messageChildrenIds = history.messages[messageId].childrenIds;
				}

				history.currentId = messageId;
			}
		}

		await tick();

Timothy J. Baek's avatar
Timothy J. Baek committed
198
		const element = document.getElementById('messages-container');
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
199
		autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
Timothy J. Baek's avatar
Timothy J. Baek committed
200

201
		setTimeout(() => {
Timothy J. Baek's avatar
Timothy J. Baek committed
202
			scrollToBottom();
203
204
		}, 100);
	};
205

Timothy J. Baek's avatar
Timothy J. Baek committed
206
	const deleteMessageHandler = async (messageId) => {
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
207
		const messageToDelete = history.messages[messageId];
Timothy J. Baek's avatar
Timothy J. Baek committed
208
209
210
211
212

		const parentMessageId = messageToDelete.parentId;
		const childMessageIds = messageToDelete.childrenIds ?? [];

		const hasDescendantMessages = childMessageIds.some(
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
213
			(childId) => history.messages[childId]?.childrenIds?.length > 0
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
214
		);
Timothy J. Baek's avatar
Timothy J. Baek committed
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232

		history.currentId = parentMessageId;
		await tick();

		// Remove the message itself from the parent message's children array
		history.messages[parentMessageId].childrenIds = history.messages[
			parentMessageId
		].childrenIds.filter((id) => id !== messageId);

		await tick();

		childMessageIds.forEach((childId) => {
			const childMessage = history.messages[childId];

			if (childMessage && childMessage.childrenIds) {
				if (childMessage.childrenIds.length === 0 && !hasDescendantMessages) {
					// If there are no other responses/prompts
					history.messages[parentMessageId].childrenIds = [];
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
233
				} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
234
					childMessage.childrenIds.forEach((grandChildId) => {
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
235
						if (history.messages[grandChildId]) {
Timothy J. Baek's avatar
Timothy J. Baek committed
236
237
							history.messages[grandChildId].parentId = parentMessageId;
							history.messages[parentMessageId].childrenIds.push(grandChildId);
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
238
239
240
241
						}
					});
				}
			}
Timothy J. Baek's avatar
Timothy J. Baek committed
242
243
244
245

			// Remove child message id from the parent message's children array
			history.messages[parentMessageId].childrenIds = history.messages[
				parentMessageId
Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
246
247
			].childrenIds.filter((id) => id !== childId);
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
248
249
250

		await tick();

Timothy J. Baek's avatar
revert  
Timothy J. Baek committed
251
252
253
254
		await updateChatById(localStorage.token, chatId, {
			messages: messages,
			history: history
		});
255
	};
256
257
</script>

Timothy J. Baek's avatar
Timothy J. Baek committed
258
<div class="h-full flex">
Timothy J. Baek's avatar
Timothy J. Baek committed
259
260
	{#if messages.length == 0}
		<Placeholder
261
			modelIds={selectedModels}
Timothy J. Baek's avatar
Timothy J. Baek committed
262
			submitPrompt={async (p) => {
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
				let text = p;

				if (p.includes('{{CLIPBOARD}}')) {
					const clipboardText = await navigator.clipboard.readText().catch((err) => {
						toast.error($i18n.t('Failed to read clipboard contents'));
						return '{{CLIPBOARD}}';
					});

					text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
				}

				prompt = text;

				await tick();

				const chatInputElement = document.getElementById('chat-textarea');
				if (chatInputElement) {
Timothy J. Baek's avatar
Timothy J. Baek committed
280
281
					prompt = p;

282
283
284
					chatInputElement.style.height = '';
					chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
					chatInputElement.focus();
Timothy J. Baek's avatar
Timothy J. Baek committed
285

286
287
288
289
290
291
					const words = findWordIndices(prompt);

					if (words.length > 0) {
						const word = words.at(0);
						chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
292
				}
293
294

				await tick();
Timothy J. Baek's avatar
Timothy J. Baek committed
295
296
297
			}}
		/>
	{:else}
Timothy J. Baek's avatar
Timothy J. Baek committed
298
		<div class="w-full pt-2">
Timothy J. Baek's avatar
Timothy J. Baek committed
299
300
			{#key chatId}
				{#each messages as message, messageIdx}
Timothy J. Baek's avatar
Timothy J. Baek committed
301
					<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
Timothy J. Baek's avatar
Timothy J. Baek committed
302
						<div
303
							class="flex flex-col justify-between px-5 mb-3 {$settings?.widescreenMode ?? null
Timothy J. Baek's avatar
Timothy J. Baek committed
304
								? 'max-w-full'
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
305
								: 'max-w-5xl'} mx-auto rounded-lg group"
Timothy J. Baek's avatar
Timothy J. Baek committed
306
307
308
						>
							{#if message.role === 'user'}
								<UserMessage
Timothy J. Baek's avatar
Timothy J. Baek committed
309
									on:delete={() => deleteMessageHandler(message.id)}
310
									{user}
Timothy J. Baek's avatar
Timothy J. Baek committed
311
312
313
314
315
316
317
318
319
320
321
322
323
									{readOnly}
									{message}
									isFirstMessage={messageIdx === 0}
									siblings={message.parentId !== null
										? history.messages[message.parentId]?.childrenIds ?? []
										: Object.values(history.messages)
												.filter((message) => message.parentId === null)
												.map((message) => message.id) ?? []}
									{confirmEditMessage}
									{showPreviousMessage}
									{showNextMessage}
									copyToClipboard={copyToClipboardWithToast}
								/>
324
							{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
Timothy J. Baek's avatar
Timothy J. Baek committed
325
								{#key message.id && history.currentId}
Timothy J. Baek's avatar
Timothy J. Baek committed
326
327
328
329
330
331
332
333
334
335
336
337
338
									<ResponseMessage
										{message}
										siblings={history.messages[message.parentId]?.childrenIds ?? []}
										isLastMessage={messageIdx + 1 === messages.length}
										{readOnly}
										{updateChatMessages}
										{confirmEditResponseMessage}
										{showPreviousMessage}
										{showNextMessage}
										{rateMessage}
										copyToClipboard={copyToClipboardWithToast}
										{continueGeneration}
										{regenerateResponse}
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
339
340
341
										on:action={async (e) => {
											await chatActionHandler(chatId, e.detail, message.model, message.id);
										}}
Timothy J. Baek's avatar
Timothy J. Baek committed
342
343
344
345
346
347
348
349
350
351
352
353
										on:save={async (e) => {
											console.log('save', e);

											const message = e.detail;
											history.messages[message.id] = message;
											await updateChatById(localStorage.token, chatId, {
												messages: messages,
												history: history
											});
										}}
									/>
								{/key}
354
355
356
357
358
							{:else}
								{#key message.parentId}
									<CompareMessages
										bind:history
										{messages}
359
										{readOnly}
360
361
362
363
364
365
366
367
368
										{chatId}
										parentMessage={history.messages[message.parentId]}
										{messageIdx}
										{updateChatMessages}
										{confirmEditResponseMessage}
										{rateMessage}
										copyToClipboard={copyToClipboardWithToast}
										{continueGeneration}
										{regenerateResponse}
Timothy J. Baek's avatar
Timothy J. Baek committed
369
370
371
372
373
374
										on:change={async () => {
											await updateChatById(localStorage.token, chatId, {
												messages: messages,
												history: history
											});

Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
375
376
377
378
379
380
381
382
											if (autoScroll) {
												const element = document.getElementById('messages-container');
												autoScroll =
													element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
												setTimeout(() => {
													scrollToBottom();
												}, 100);
											}
383
384
385
										}}
									/>
								{/key}
Timothy J. Baek's avatar
Timothy J. Baek committed
386
387
							{/if}
						</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
388
					</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
389
390
391
				{/each}

				{#if bottomPadding}
Timothy J. Baek's avatar
Timothy J. Baek committed
392
					<div class="  pb-6" />
Timothy J. Baek's avatar
Timothy J. Baek committed
393
394
395
396
397
				{/if}
			{/key}
		</div>
	{/if}
</div>