File size: 2,530 Bytes
502cb81
0ce274f
1778c9e
0ce274f
 
b2170a7
b1426d3
f48efbb
502cb81
5851d63
1778c9e
5851d63
1778c9e
5851d63
502cb81
1778c9e
5851d63
2dd779d
 
 
5851d63
0ce274f
5c869f5
2dd779d
1778c9e
2dd779d
0ce274f
2dd779d
0ce274f
 
 
 
 
 
 
5851d63
2dd779d
b2170a7
 
1778c9e
 
 
 
 
 
 
 
 
 
 
b2170a7
f36471e
1778c9e
 
 
f36471e
 
1778c9e
f36471e
1778c9e
f36471e
 
1778c9e
 
f36471e
502cb81
5213b80
502cb81
28faefd
1778c9e
5213b80
502cb81
5213b80
1778c9e
f48efbb
1778c9e
 
899d9c6
1778c9e
 
 
f48efbb
5213b80
 
502cb81
eeca96c
5851d63
1778c9e
502cb81
8c5a2cf
64cfbce
 
 
b2170a7
5213b80
502cb81
5213b80
1778c9e
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
<script lang="ts">
	import { ScrollState } from "$lib/spells/scroll-state.svelte";
	import { type ConversationClass } from "$lib/state/conversations.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";

	interface Props {
		conversation: ConversationClass;
		viewCode: boolean;
		onCloseCode: () => void;
	}

	const { conversation, viewCode, onCloseCode }: 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.data.messages.at(-1)?.content,
		() => {
			const shouldScroll = atBottom && !scrollState.isScrolling;
			if (!shouldScroll) return;
			try {
				tick().then(() => {
					scrollState.scrollToBottom();
				});
			} catch {
				// noop
			}
		}
	);

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

	async function regenMessage(idx: number) {
		// TODO: migrate to new logic
		const msg = conversation.data.messages[idx];
		if (!msg) return;
		if (msg.role === "user") {
			await conversation.deleteMessages(idx + 1);
		} else {
			await conversation.deleteMessages(idx);
		}

		conversation.stopGenerating();
		conversation.genNextMessage();
	}
</script>

<div
	class="@container flex flex-col overflow-x-hidden overflow-y-auto"
	class:animate-pulse={conversation.generating && !conversation.data.streaming}
	bind:this={messageContainer}
>
	{#if !viewCode}
		{#each conversation.data.messages as message, index}
			<Message
				{message}
				{index}
				{conversation}
				autofocus={index === conversation.data.messages.length - 1}
				onDelete={() => conversation.deleteMessage(index)}
				onRegen={() => regenMessage(index)}
			/>
		{/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={conversation.generating}
		>
			<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} {onCloseCode} />
	{/if}
</div>