File size: 2,310 Bytes
a3ae6ee
c3aa4e3
fc15a4c
3aa8136
 
8811ee0
697285c
3aa8136
bd8c7a0
0ca99f1
 
 
 
bd8c7a0
3aa8136
 
 
 
 
8811ee0
 
 
697285c
8811ee0
 
697285c
3aa8136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3ae6ee
 
1b66f8d
a3ae6ee
 
 
 
 
 
 
3aa8136
a3ae6ee
3aa8136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3ae6ee
 
 
 
 
 
7b1d57f
 
a3ae6ee
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<script lang="ts">
	import { marked } from 'marked';
	import type { Message } from '$lib/types/Message';
	import { afterUpdate } from 'svelte';
	import { deepestChild } from '$lib/utils/dom';

	import CodeBlock from '../CodeBlock.svelte';
	import IconLoading from '../icons/IconLoading.svelte';

	function sanitizeMd(md: string) {
		return md.replaceAll('<', '&lt;');
	}

	export let message: Message;
	export let loading: boolean = false;

	let contentEl: HTMLElement;
	let loadingEl: any;
	let pendingTimeout: NodeJS.Timeout;

	const options: marked.MarkedOptions = {
		...marked.getDefaults(),
		gfm: true
	};

	$: tokens = marked.lexer(sanitizeMd(message.content));

	afterUpdate(() => {
		loadingEl?.$destroy();
		clearTimeout(pendingTimeout);

		// Add loading animation to the last message if update takes more than 600ms
		if (loading) {
			pendingTimeout = setTimeout(() => {
				if (contentEl) {
					loadingEl = new IconLoading({
						target: deepestChild(contentEl),
						props: { classNames: 'loading inline ml-2' }
					});
				}
			}, 600);
		}
	});
</script>

{#if message.from === 'assistant'}
	<div class="flex items-start justify-start gap-4 leading-relaxed">
		<img
			alt=""
			src="https://huggingface.co/avatars/2edb18bd0206c16b433841a47f53fa8e.svg"
			class="mt-5 w-3 h-3 flex-none rounded-full shadow-lg"
		/>
		<div
			class="relative rounded-2xl px-5 py-3.5 border border-gray-100 bg-gradient-to-br from-gray-50 dark:from-gray-800/40 dark:border-gray-800 text-gray-600 dark:text-gray-300 min-h-[calc(2rem+theme(spacing[3.5])*2)] min-w-[100px]"
		>
			{#if !message.content}
				<IconLoading classNames="absolute inset-0 m-auto" />
			{/if}
			<div
				class="prose dark:prose-invert :prose-pre:bg-gray-100 dark:prose-pre:bg-gray-950"
				bind:this={contentEl}
			>
				{#each tokens as token}
					{#if token.type === 'code'}
						<CodeBlock lang={token.lang} code={token.text} />
					{:else}
						{@html marked.parser([token], options)}
					{/if}
				{/each}
			</div>
		</div>
	</div>
{/if}
{#if message.from === 'user'}
	<div class="flex items-start justify-start gap-4">
		<div class="mt-5 w-3 h-3 flex-none rounded-full" />
		<div class="rounded-2xl px-5 py-3.5 text-gray-500 dark:text-gray-400 whitespace-break-spaces">
			{message.content.trim()}
		</div>
	</div>
{/if}