ResponseMessage.svelte 21.3 KB
Newer Older
1
<script lang="ts">
Timothy J. Baek's avatar
Timothy J. Baek committed
2
	import toast from 'svelte-french-toast';
Timothy J. Baek's avatar
Timothy J. Baek committed
3
	import dayjs from 'dayjs';
4
	import { marked } from 'marked';
Timothy J. Baek's avatar
Timothy J. Baek committed
5
	import { settings } from '$lib/stores';
6
7
8
9
	import tippy from 'tippy.js';
	import auto_render from 'katex/dist/contrib/auto-render.mjs';
	import 'katex/dist/katex.min.css';

Timothy J. Baek's avatar
Timothy J. Baek committed
10
11
	import { onMount, tick } from 'svelte';

12
13
14
	import Name from './Name.svelte';
	import ProfileImage from './ProfileImage.svelte';
	import Skeleton from './Skeleton.svelte';
Timothy J. Baek's avatar
Timothy J. Baek committed
15
	import CodeBlock from './CodeBlock.svelte';
16

Timothy J. Baek's avatar
Timothy J. Baek committed
17
	import { synthesizeOpenAISpeech } from '$lib/apis/openai';
Timothy J. Baek's avatar
Timothy J. Baek committed
18
	import { extractSentences } from '$lib/utils';
Timothy J. Baek's avatar
Timothy J. Baek committed
19

20
21
22
23
24
25
26
27
28
29
30
31
	export let modelfiles = [];
	export let message;
	export let siblings;

	export let isLastMessage = true;

	export let confirmEditResponseMessage: Function;
	export let showPreviousMessage: Function;
	export let showNextMessage: Function;
	export let rateMessage: Function;

	export let copyToClipboard: Function;
Timothy J. Baek's avatar
Timothy J. Baek committed
32
	export let continueGeneration: Function;
33
34
35
36
37
38
	export let regenerateResponse: Function;

	let edit = false;
	let editedContent = '';

	let tooltipInstance = null;
39

Timothy J. Baek's avatar
Timothy J. Baek committed
40
	let sentencesAudio = {};
41
	let speaking = null;
Timothy J. Baek's avatar
Timothy J. Baek committed
42
43
	let speakingIdx = null;

44
	let loadingSpeech = false;
45

Timothy J. Baek's avatar
Timothy J. Baek committed
46
47
48
49
50
51
52
53
54
55
56
57
58
59
	$: tokens = marked.lexer(message.content);

	const renderer = new marked.Renderer();

	// For code blocks with simple backticks
	renderer.codespan = (code) => {
		return `<code>${code.replaceAll('&amp;', '&')}</code>`;
	};

	const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		extensions: any;
	};

60
61
62
63
64
65
66
67
	$: if (message) {
		renderStyling();
	}

	const renderStyling = async () => {
		await tick();

		if (tooltipInstance) {
68
			tooltipInstance[0]?.destroy();
69
70
71
72
73
74
		}

		renderLatex();

		if (message.info) {
			tooltipInstance = tippy(`#info-${message.id}`, {
75
				content: `<span class="text-xs" id="tooltip-${message.id}">response_token/s: ${
76
77
78
79
80
81
					`${
						Math.round(
							((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100
						) / 100
					} tokens` ?? 'N/A'
				}<br/>
82
					prompt_token/s: ${
Danny Liu's avatar
Danny Liu committed
83
84
85
						Math.round(
							((message.info.prompt_eval_count ?? 0) / (message.info.prompt_eval_duration / 1000000000)) * 100
						) / 100 ?? 'N/A'
86
					} tokens<br/>
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
                    total_duration: ${
											Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ??
											'N/A'
										}ms<br/>
                    load_duration: ${
											Math.round(((message.info.load_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
										}ms<br/>
                    prompt_eval_count: ${message.info.prompt_eval_count ?? 'N/A'}<br/>
                    prompt_eval_duration: ${
											Math.round(((message.info.prompt_eval_duration ?? 0) / 1000000) * 100) /
												100 ?? 'N/A'
										}ms<br/>
                    eval_count: ${message.info.eval_count ?? 'N/A'}<br/>
                    eval_duration: ${
											Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
										}ms</span>`,
				allowHTML: true
			});
		}
	};

	const renderLatex = () => {
		let chatMessageElements = document.getElementsByClassName('chat-assistant');
		// let lastChatMessageElement = chatMessageElements[chatMessageElements.length - 1];

		for (const element of chatMessageElements) {
			auto_render(element, {
				// customised options
				// • auto-render specific keys, e.g.:
				delimiters: [
					{ left: '$$', right: '$$', display: true },
					// { left: '$', right: '$', display: false },
					{ left: '\\(', right: '\\)', display: true },
					{ left: '\\[', right: '\\]', display: true }
				],
				// • rendering keys, e.g.:
				throwOnError: false
			});
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
128
129
130
131
132
133
	const playAudio = (idx) => {
		return new Promise((res) => {
			speakingIdx = idx;
			const audio = sentencesAudio[idx];
			audio.play();
			audio.onended = async (e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
134
				await new Promise((r) => setTimeout(r, 300));
Timothy J. Baek's avatar
Timothy J. Baek committed
135
136
137

				if (Object.keys(sentencesAudio).length - 1 === idx) {
					speaking = null;
Timothy J. Baek's avatar
Timothy J. Baek committed
138
139
140
141

					if ($settings.conversationMode) {
						document.getElementById('voice-input-button')?.click();
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
142
143
144
145
146
147
148
				}

				res(e);
			};
		});
	};

149
150
151
	const toggleSpeakMessage = async () => {
		if (speaking) {
			speechSynthesis.cancel();
Timothy J. Baek's avatar
Timothy J. Baek committed
152

Timothy J. Baek's avatar
Timothy J. Baek committed
153
154
155
156
157
			sentencesAudio[speakingIdx].pause();
			sentencesAudio[speakingIdx].currentTime = 0;

			speaking = null;
			speakingIdx = null;
158
159
		} else {
			speaking = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
160

Timothy J. Baek's avatar
Timothy J. Baek committed
161
			if ($settings?.audio?.TTSEngine === 'openai') {
162
				loadingSpeech = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
163

164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
				const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
					const lastIndex = mergedTexts.length - 1;
					if (lastIndex >= 0) {
						const previousText = mergedTexts[lastIndex];
						const wordCount = previousText.split(/\s+/).length;
						if (wordCount < 2) {
							mergedTexts[lastIndex] = previousText + ' ' + currentText;
						} else {
							mergedTexts.push(currentText);
						}
					} else {
						mergedTexts.push(currentText);
					}
					return mergedTexts;
				}, []);

Timothy J. Baek's avatar
Timothy J. Baek committed
180
181
182
183
184
185
186
187
188
189
190
191
				console.log(sentences);

				sentencesAudio = sentences.reduce((a, e, i, arr) => {
					a[i] = null;
					return a;
				}, {});

				let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately

				for (const [idx, sentence] of sentences.entries()) {
					const res = await synthesizeOpenAISpeech(
						localStorage.token,
Timothy J. Baek's avatar
Timothy J. Baek committed
192
						$settings?.audio?.speaker,
Timothy J. Baek's avatar
Timothy J. Baek committed
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
						sentence
					).catch((error) => {
						toast.error(error);
						return null;
					});

					if (res) {
						const blob = await res.blob();
						const blobUrl = URL.createObjectURL(blob);
						const audio = new Audio(blobUrl);
						sentencesAudio[idx] = audio;
						loadingSpeech = false;

						lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
					}
Timothy J. Baek's avatar
Timothy J. Baek committed
208
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
209
210
211
212
213
214
215
216
			} else {
				let voices = [];
				const getVoicesLoop = setInterval(async () => {
					voices = await speechSynthesis.getVoices();
					if (voices.length > 0) {
						clearInterval(getVoicesLoop);

						const voice =
Timothy J. Baek's avatar
Timothy J. Baek committed
217
							voices?.filter((v) => v.name === $settings?.audio?.speaker)?.at(0) ?? undefined;
Timothy J. Baek's avatar
Timothy J. Baek committed
218
219
220
221
222

						const speak = new SpeechSynthesisUtterance(message.content);

						speak.onend = () => {
							speaking = null;
Timothy J. Baek's avatar
Timothy J. Baek committed
223
224
225
							if ($settings.conversationMode) {
								document.getElementById('voice-input-button')?.click();
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
226
227
228
229
230
231
						};
						speak.voice = voice;
						speechSynthesis.speak(speak);
					}
				}, 100);
			}
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
		}
	};

	const editMessageHandler = async () => {
		edit = true;
		editedContent = message.content;

		await tick();
		const editElement = document.getElementById(`message-edit-${message.id}`);

		editElement.style.height = '';
		editElement.style.height = `${editElement.scrollHeight}px`;
	};

	const editMessageConfirmHandler = async () => {
247
248
249
250
		if (editedContent === '') {
			editedContent = ' ';
		}

251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
		confirmEditResponseMessage(message.id, editedContent);

		edit = false;
		editedContent = '';

		await tick();
		renderStyling();
	};

	const cancelEditMessage = async () => {
		edit = false;
		editedContent = '';
		await tick();
		renderStyling();
	};

	onMount(async () => {
		await tick();
		renderStyling();
	});
</script>

273
274
275
276
277
278
279
280
281
{#key message.id}
	<div class=" flex w-full message-{message.id}">
		<ProfileImage src={modelfiles[message.model]?.imageUrl ?? '/favicon.png'} />

		<div class="w-full overflow-hidden">
			<Name>
				{#if message.model in modelfiles}
					{modelfiles[message.model]?.title}
				{:else}
Timothy J. Baek's avatar
Timothy J. Baek committed
282
					{message.model ? ` ${message.model}` : ''}
283
284
285
286
				{/if}

				{#if message.timestamp}
					<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
Timothy J. Baek's avatar
Timothy J. Baek committed
287
						{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')}
288
289
290
291
292
293
					</span>
				{/if}
			</Name>

			{#if message.content === ''}
				<Skeleton />
294
			{:else}
295
				<div
Timothy J. Baek's avatar
Timothy J. Baek committed
296
					class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-li:-mb-4 whitespace-pre-line"
297
				>
298
299
300
301
302
303
304
305
306
					<div>
						{#if edit === true}
							<div class=" w-full">
								<textarea
									id="message-edit-{message.id}"
									class=" bg-transparent outline-none w-full resize-none"
									bind:value={editedContent}
									on:input={(e) => {
										e.target.style.height = `${e.target.scrollHeight}px`;
307
									}}
308
								/>
309

310
								<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
311
									<button
312
										class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
313
										on:click={() => {
314
											editMessageConfirmHandler();
315
316
										}}
									>
317
										Save
318
319
320
									</button>

									<button
321
										class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
322
										on:click={() => {
323
											cancelEditMessage();
324
										}}
325
326
327
328
329
330
331
332
333
334
									>
										Cancel
									</button>
								</div>
							</div>
						{:else}
							<div class="w-full">
								{#if message?.error === true}
									<div
										class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
335
336
337
338
339
340
341
									>
										<svg
											xmlns="http://www.w3.org/2000/svg"
											fill="none"
											viewBox="0 0 24 24"
											stroke-width="1.5"
											stroke="currentColor"
342
											class="w-5 h-5 self-center"
343
344
345
346
										>
											<path
												stroke-linecap="round"
												stroke-linejoin="round"
347
												d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
348
349
350
											/>
										</svg>

351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
										<div class=" self-center">
											{message.content}
										</div>
									</div>
								{:else}
									{#each tokens as token}
										{#if token.type === 'code'}
											<!-- {token.text} -->
											<CodeBlock lang={token.lang} code={token.text} />
										{:else}
											{@html marked.parse(token.raw, {
												...defaults,
												gfm: true,
												breaks: true,
												renderer
											})}
										{/if}
									{/each}
									<!-- {@html marked(message.content.replaceAll('\\', '\\\\'))} -->
								{/if}

								{#if message.done}
373
									<div
374
										class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
375
									>
376
										{#if siblings.length > 1}
Timothy J. Baek's avatar
Timothy J. Baek committed
377
											<div class="flex self-center min-w-fit">
378
												<button
379
													class="self-center dark:hover:text-white hover:text-black transition"
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
													on:click={() => {
														showPreviousMessage(message);
													}}
												>
													<svg
														xmlns="http://www.w3.org/2000/svg"
														viewBox="0 0 20 20"
														fill="currentColor"
														class="w-4 h-4"
													>
														<path
															fill-rule="evenodd"
															d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
															clip-rule="evenodd"
														/>
													</svg>
												</button>

398
												<div class="text-xs font-bold self-center min-w-fit dark:text-gray-100">
399
400
401
402
													{siblings.indexOf(message.id) + 1} / {siblings.length}
												</div>

												<button
403
													class="self-center dark:hover:text-white hover:text-black transition"
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
													on:click={() => {
														showNextMessage(message);
													}}
												>
													<svg
														xmlns="http://www.w3.org/2000/svg"
														viewBox="0 0 20 20"
														fill="currentColor"
														class="w-4 h-4"
													>
														<path
															fill-rule="evenodd"
															d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
															clip-rule="evenodd"
														/>
													</svg>
												</button>
											</div>
										{/if}
423

424
425
426
										<button
											class="{isLastMessage
												? 'visible'
427
												: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
428
429
430
431
											on:click={() => {
												editMessageHandler();
											}}
										>
432
433
434
435
436
437
438
439
440
441
442
											<svg
												xmlns="http://www.w3.org/2000/svg"
												fill="none"
												viewBox="0 0 24 24"
												stroke-width="1.5"
												stroke="currentColor"
												class="w-4 h-4"
											>
												<path
													stroke-linecap="round"
													stroke-linejoin="round"
443
													d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
444
445
												/>
											</svg>
446
447
448
449
450
										</button>

										<button
											class="{isLastMessage
												? 'visible'
451
												: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
452
453
454
455
											on:click={() => {
												copyToClipboard(message.content);
											}}
										>
456
457
458
459
460
461
462
463
464
465
466
											<svg
												xmlns="http://www.w3.org/2000/svg"
												fill="none"
												viewBox="0 0 24 24"
												stroke-width="1.5"
												stroke="currentColor"
												class="w-4 h-4"
											>
												<path
													stroke-linecap="round"
													stroke-linejoin="round"
467
													d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
468
469
												/>
											</svg>
470
										</button>
471
472

										<button
473
											class="{isLastMessage
474
												? 'visible'
475
												: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
Timothy J. Baek's avatar
Timothy J. Baek committed
476
												? 'bg-gray-100 dark:bg-gray-800'
477
												: ''} dark:hover:text-white hover:text-black transition"
478
											on:click={() => {
479
												rateMessage(message.id, 1);
480
481
482
											}}
										>
											<svg
483
												stroke="currentColor"
484
												fill="none"
485
												stroke-width="2"
486
												viewBox="0 0 24 24"
487
488
												stroke-linecap="round"
												stroke-linejoin="round"
489
												class="w-4 h-4"
490
491
492
493
												xmlns="http://www.w3.org/2000/svg"
												><path
													d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
												/></svg
494
495
496
497
498
											>
										</button>
										<button
											class="{isLastMessage
												? 'visible'
499
												: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
Timothy J. Baek's avatar
Timothy J. Baek committed
500
												? 'bg-gray-100 dark:bg-gray-800'
501
												: ''} dark:hover:text-white hover:text-black transition"
502
503
504
											on:click={() => {
												rateMessage(message.id, -1);
											}}
505
506
										>
											<svg
507
												stroke="currentColor"
508
												fill="none"
509
												stroke-width="2"
510
												viewBox="0 0 24 24"
511
512
												stroke-linecap="round"
												stroke-linejoin="round"
513
												class="w-4 h-4"
514
515
516
517
												xmlns="http://www.w3.org/2000/svg"
												><path
													d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
												/></svg
518
519
											>
										</button>
520
521

										<button
Timothy J. Baek's avatar
Timothy J. Baek committed
522
											id="speak-button-{message.id}"
523
524
											class="{isLastMessage
												? 'visible'
525
												: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
526
											on:click={() => {
527
528
529
												if (!loadingSpeech) {
													toggleSpeakMessage(message);
												}
530
531
											}}
										>
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
											{#if loadingSpeech}
												<svg
													class=" w-4 h-4"
													fill="currentColor"
													viewBox="0 0 24 24"
													xmlns="http://www.w3.org/2000/svg"
													><style>
														.spinner_S1WN {
															animation: spinner_MGfb 0.8s linear infinite;
															animation-delay: -0.8s;
														}
														.spinner_Km9P {
															animation-delay: -0.65s;
														}
														.spinner_JApP {
															animation-delay: -0.5s;
														}
														@keyframes spinner_MGfb {
															93.75%,
															100% {
																opacity: 0.2;
															}
														}
													</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
														class="spinner_S1WN spinner_Km9P"
														cx="12"
														cy="12"
														r="3"
													/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
												>
											{:else if speaking}
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
												<svg
													xmlns="http://www.w3.org/2000/svg"
													fill="none"
													viewBox="0 0 24 24"
													stroke-width="1.5"
													stroke="currentColor"
													class="w-4 h-4"
												>
													<path
														stroke-linecap="round"
														stroke-linejoin="round"
														d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
													/>
												</svg>
											{:else}
												<svg
													xmlns="http://www.w3.org/2000/svg"
													fill="none"
													viewBox="0 0 24 24"
													stroke-width="1.5"
													stroke="currentColor"
													class="w-4 h-4"
												>
													<path
														stroke-linecap="round"
														stroke-linejoin="round"
														d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
													/>
												</svg>
											{/if}
										</button>

										{#if message.info}
											<button
												class=" {isLastMessage
													? 'visible'
599
													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
												on:click={() => {
													console.log(message);
												}}
												id="info-{message.id}"
											>
												<svg
													xmlns="http://www.w3.org/2000/svg"
													fill="none"
													viewBox="0 0 24 24"
													stroke-width="1.5"
													stroke="currentColor"
													class="w-4 h-4"
												>
													<path
														stroke-linecap="round"
														stroke-linejoin="round"
														d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
													/>
												</svg>
											</button>
										{/if}

										{#if isLastMessage}
Timothy J. Baek's avatar
Timothy J. Baek committed
623
624
625
626
											<button
												type="button"
												class="{isLastMessage
													? 'visible'
627
													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
Timothy J. Baek's avatar
Timothy J. Baek committed
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
												on:click={() => {
													continueGeneration();
												}}
											>
												<svg
													xmlns="http://www.w3.org/2000/svg"
													fill="none"
													viewBox="0 0 24 24"
													stroke-width="1.5"
													stroke="currentColor"
													class="w-4 h-4"
												>
													<path
														stroke-linecap="round"
														stroke-linejoin="round"
														d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
													/>
													<path
														stroke-linecap="round"
														stroke-linejoin="round"
														d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
													/>
												</svg>
											</button>

653
654
655
656
											<button
												type="button"
												class="{isLastMessage
													? 'visible'
657
													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
												on:click={regenerateResponse}
											>
												<svg
													xmlns="http://www.w3.org/2000/svg"
													fill="none"
													viewBox="0 0 24 24"
													stroke-width="1.5"
													stroke="currentColor"
													class="w-4 h-4"
												>
													<path
														stroke-linecap="round"
														stroke-linejoin="round"
														d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
													/>
												</svg>
											</button>
										{/if}
									</div>
								{/if}
							</div>
						{/if}
					</div>
681
				</div>
682
683
			{/if}
		</div>
684
	</div>
685
{/key}
Timothy J. Baek's avatar
Timothy J. Baek committed
686
687
688
689
690
691
692
693
694
695
696

<style>
	.buttons::-webkit-scrollbar {
		display: none; /* for Chrome, Safari and Opera */
	}

	.buttons {
		-ms-overflow-style: none; /* IE and Edge */
		scrollbar-width: none; /* Firefox */
	}
</style>