summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/components/ChatMessage.svelte
blob: c6b6034c6a23de8ad1703989f93bb52686087750 (plain)
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
<script lang="ts">
import { appSettings } from "../settings.svelte.js";
import { tabStore } from "../tabs.svelte.js";
import type { ChatMessage } from "../types.js";
import MarkdownRenderer from "./MarkdownRenderer.svelte";
import ToolCallDisplay from "./ToolCallDisplay.svelte";

const { message, tabId }: { message: ChatMessage; tabId?: string } = $props();

const isUser = $derived(message.role === "user");
const isSystem = $derived(message.role === "system");

// Check if this message is queued: its id starts with "queued-"
const queuedMessageId = $derived(
	isUser && message.id.startsWith("queued-") ? message.id.slice("queued-".length) : null,
);
const isQueued = $derived(queuedMessageId !== null);

function cancelQueued() {
	if (tabId && queuedMessageId) {
		void tabStore.cancelQueuedMessage(tabId, queuedMessageId);
	}
}
</script>

{#if isSystem}
	<div class="flex justify-center my-2">
		<div class="badge badge-ghost gap-1 text-xs opacity-60">
			{#each message.content as segment}
				{#if segment.type === "text"}
					{segment.text}
				{/if}
			{/each}
		</div>
	</div>
{:else}
<div class="chat chat-start mb-2 [&>.chat-bubble]:max-w-full {isQueued ? 'opacity-60' : ''}">
	<div class="chat-bubble break-words {isUser ? 'chat-bubble-primary w-fit' : 'bg-transparent w-full'}">
		{#if message.thinking}
			<div class="collapse collapse-arrow mb-2 p-1">
				<input type="checkbox" checked={appSettings.autoExpandThinking} />
				<div class="collapse-title text-sm opacity-60 italic py-0 pl-0 pr-8 min-h-0">Thinking...</div>
				<div class="collapse-content text-sm opacity-60 italic p-0">
					<p class="whitespace-pre-wrap mt-1">{message.thinking}</p>
				</div>
			</div>
		{/if}
		{#each message.content as segment, i (segment.type === "tool-call" ? segment.id : i)}
			{#if segment.type === "text"}
				<MarkdownRenderer text={segment.text} streaming={message.isStreaming} />
			{:else if segment.type === "tool-call"}
				<ToolCallDisplay toolCall={segment} />
			{/if}
		{/each}
		{#if message.isStreaming}
			<span class="inline-block w-1.5 h-4 bg-current animate-pulse ml-0.5 align-middle rounded-sm"></span>
		{/if}
	</div>
	{#if isQueued}
		<div class="flex items-center gap-1 mt-0.5 ml-1">
			<span class="badge badge-ghost badge-xs text-base-content/40">queued</span>
			<button
				class="btn btn-xs btn-ghost text-base-content/40 hover:text-error px-1 min-h-0 h-auto"
				onclick={cancelQueued}
				title="Cancel queued message"
			>
				✕
			</button>
		</div>
	{/if}
</div>
{/if}