File size: 2,690 Bytes
502cb81
97c4991
4b8b411
0ce274f
 
 
b2170a7
b1426d3
f48efbb
f36471e
 
502cb81
5851d63
 
 
 
 
502cb81
28faefd
5851d63
2dd779d
 
 
5851d63
0ce274f
5c869f5
2dd779d
 
 
0ce274f
2dd779d
0ce274f
 
 
 
 
 
 
5851d63
2dd779d
b2170a7
 
 
 
 
 
 
 
 
 
 
 
 
 
9b4caaa
b2170a7
f36471e
 
 
 
 
 
 
 
 
 
 
 
 
502cb81
5213b80
502cb81
28faefd
d5e14b5
5213b80
2dd779d
502cb81
5213b80
f36471e
f48efbb
899d9c6
 
f48efbb
 
 
f36471e
 
f48efbb
5213b80
 
502cb81
eeca96c
5851d63
5213b80
502cb81
8c5a2cf
64cfbce
 
 
b2170a7
5213b80
502cb81
5213b80
25c63d0
5213b80
 
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<script lang="ts">
	import { type Conversation } from "$lib/types.js";

	import { ScrollState } from "$lib/spells/scroll-state.svelte";
	import { watch } from "runed";
	import { tick } from "svelte";
	import IconPlus from "~icons/carbon/add";
	import CodeSnippets from "./code-snippets.svelte";
	import Message from "./message.svelte";
	import { iterate } from "$lib/utils/array.js";
	import { session } from "$lib/state/session.svelte";

	interface Props {
		conversation: Conversation;
		loading: boolean;
		viewCode: boolean;
	}

	let { conversation = $bindable(), loading, viewCode }: Props = $props();
	let messageContainer: HTMLDivElement | null = $state(null);
	const scrollState = new ScrollState({
		element: () => messageContainer,
		offset: { bottom: 100 },
	});
	const atBottom = $derived(scrollState.arrived.bottom);

	watch(
		() => conversation.messages.at(-1)?.content,
		() => {
			const shouldScroll = atBottom && !scrollState.isScrolling;
			if (!shouldScroll) return;
			try {
				tick().then(() => {
					scrollState.scrollToBottom();
				});
			} catch {
				// noop
			}
		}
	);

	function addMessage() {
		const msgs = conversation.messages.slice();
		conversation.messages = [
			...msgs,
			{
				role: msgs.at(-1)?.role === "user" ? "assistant" : "user",
				content: "",
			},
		];
		conversation = conversation;
	}

	function deleteMessage(idx: number) {
		conversation.messages = conversation.messages.slice(0, idx);
	}

	function regenMessage(idx: number) {
		const msg = conversation.messages[idx];
		if (!msg) return;
		if (msg.role === "user") {
			conversation.messages = conversation.messages.slice(0, idx + 1);
		} else {
			conversation.messages = conversation.messages.slice(0, idx);
		}

		session.stopGenerating();
		session.run(conversation);
	}
</script>

<div
	class="@container flex flex-col overflow-x-hidden overflow-y-auto"
	class:animate-pulse={loading && !conversation.streaming}
	bind:this={messageContainer}
	id="test-this"
>
	{#if !viewCode}
		{#each iterate(conversation.messages) as [_msg, { isLast }], idx}
			<Message
				bind:message={conversation.messages[idx]!}
				{conversation}
				autofocus={idx === conversation.messages.length - 1}
				{loading}
				onDelete={() => deleteMessage(idx)}
				onRegen={() => regenMessage(idx)}
				{isLast}
			/>
		{/each}

		<button
			class="flex px-3.5 py-6 hover:bg-gray-50 md:px-6 dark:hover:bg-gray-800/50"
			onclick={addMessage}
			disabled={loading}
		>
			<div class="flex items-center gap-2 p-0! text-sm font-semibold">
				<div class="text-lg">
					<IconPlus />
				</div>
				Add message
			</div>
		</button>
	{:else}
		<CodeSnippets {conversation} on:closeCode />
	{/if}
</div>