1<script lang="ts">
2 import { Card } from '$lib/components/ui/card';
3 import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
4 import { config } from '$lib/stores/settings.svelte';
5 import ChatMessageActions from './ChatMessageActions.svelte';
6 import ChatMessageEditForm from './ChatMessageEditForm.svelte';
7
8 interface Props {
9 class?: string;
10 message: DatabaseMessage;
11 isEditing: boolean;
12 editedContent: string;
13 editedExtras?: DatabaseMessageExtra[];
14 editedUploadedFiles?: ChatUploadedFile[];
15 siblingInfo?: ChatMessageSiblingInfo | null;
16 showDeleteDialog: boolean;
17 deletionInfo: {
18 totalCount: number;
19 userMessages: number;
20 assistantMessages: number;
21 messageTypes: string[];
22 } | null;
23 onCancelEdit: () => void;
24 onSaveEdit: () => void;
25 onSaveEditOnly?: () => void;
26 onEditKeydown: (event: KeyboardEvent) => void;
27 onEditedContentChange: (content: string) => void;
28 onEditedExtrasChange?: (extras: DatabaseMessageExtra[]) => void;
29 onEditedUploadedFilesChange?: (files: ChatUploadedFile[]) => void;
30 onCopy: () => void;
31 onEdit: () => void;
32 onDelete: () => void;
33 onConfirmDelete: () => void;
34 onNavigateToSibling?: (siblingId: string) => void;
35 onShowDeleteDialogChange: (show: boolean) => void;
36 textareaElement?: HTMLTextAreaElement;
37 }
38
39 let {
40 class: className = '',
41 message,
42 isEditing,
43 editedContent,
44 editedExtras = [],
45 editedUploadedFiles = [],
46 siblingInfo = null,
47 showDeleteDialog,
48 deletionInfo,
49 onCancelEdit,
50 onSaveEdit,
51 onSaveEditOnly,
52 onEditKeydown,
53 onEditedContentChange,
54 onEditedExtrasChange,
55 onEditedUploadedFilesChange,
56 onCopy,
57 onEdit,
58 onDelete,
59 onConfirmDelete,
60 onNavigateToSibling,
61 onShowDeleteDialogChange,
62 textareaElement = $bindable()
63 }: Props = $props();
64
65 let isMultiline = $state(false);
66 let messageElement: HTMLElement | undefined = $state();
67 const currentConfig = config();
68
69 $effect(() => {
70 if (!messageElement || !message.content.trim()) return;
71
72 if (message.content.includes('\n')) {
73 isMultiline = true;
74 return;
75 }
76
77 const resizeObserver = new ResizeObserver((entries) => {
78 for (const entry of entries) {
79 const element = entry.target as HTMLElement;
80 const estimatedSingleLineHeight = 24; // Typical line height for text-md
81
82 isMultiline = element.offsetHeight > estimatedSingleLineHeight * 1.5;
83 }
84 });
85
86 resizeObserver.observe(messageElement);
87
88 return () => {
89 resizeObserver.disconnect();
90 };
91 });
92</script>
93
94<div
95 aria-label="User message with actions"
96 class="group flex flex-col items-end gap-3 md:gap-2 {className}"
97 role="group"
98>
99 {#if isEditing}
100 <ChatMessageEditForm
101 bind:textareaElement
102 messageId={message.id}
103 {editedContent}
104 {editedExtras}
105 {editedUploadedFiles}
106 originalContent={message.content}
107 originalExtras={message.extra}
108 showSaveOnlyOption={!!onSaveEditOnly}
109 {onCancelEdit}
110 {onSaveEdit}
111 {onSaveEditOnly}
112 {onEditKeydown}
113 {onEditedContentChange}
114 {onEditedExtrasChange}
115 {onEditedUploadedFilesChange}
116 />
117 {:else}
118 {#if message.extra && message.extra.length > 0}
119 <div class="mb-2 max-w-[80%]">
120 <ChatAttachmentsList attachments={message.extra} readonly={true} imageHeight="h-80" />
121 </div>
122 {/if}
123
124 {#if message.content.trim()}
125 <Card
126 class="max-w-[80%] rounded-[1.125rem] border-none bg-primary px-3.75 py-1.5 text-primary-foreground data-[multiline]:py-2.5"
127 data-multiline={isMultiline ? '' : undefined}
128 >
129 {#if currentConfig.renderUserContentAsMarkdown}
130 <div bind:this={messageElement} class="text-md">
131 <MarkdownContent
132 class="markdown-user-content text-primary-foreground"
133 content={message.content}
134 />
135 </div>
136 {:else}
137 <span bind:this={messageElement} class="text-md whitespace-pre-wrap">
138 {message.content}
139 </span>
140 {/if}
141 </Card>
142 {/if}
143
144 {#if message.timestamp}
145 <div class="max-w-[80%]">
146 <ChatMessageActions
147 actionsPosition="right"
148 {deletionInfo}
149 justify="end"
150 {onConfirmDelete}
151 {onCopy}
152 {onDelete}
153 {onEdit}
154 {onNavigateToSibling}
155 {onShowDeleteDialogChange}
156 {siblingInfo}
157 {showDeleteDialog}
158 role="user"
159 />
160 </div>
161 {/if}
162 {/if}
163</div>