1<script lang="ts">
  2	import { chatStore } from '$lib/stores/chat.svelte';
  3	import { config } from '$lib/stores/settings.svelte';
  4	import { copyToClipboard, isIMEComposing, formatMessageForClipboard } from '$lib/utils';
  5	import ChatMessageAssistant from './ChatMessageAssistant.svelte';
  6	import ChatMessageUser from './ChatMessageUser.svelte';
  7	import ChatMessageSystem from './ChatMessageSystem.svelte';
  8
  9	interface Props {
 10		class?: string;
 11		message: DatabaseMessage;
 12		onCopy?: (message: DatabaseMessage) => void;
 13		onContinueAssistantMessage?: (message: DatabaseMessage) => void;
 14		onDelete?: (message: DatabaseMessage) => void;
 15		onEditWithBranching?: (
 16			message: DatabaseMessage,
 17			newContent: string,
 18			newExtras?: DatabaseMessageExtra[]
 19		) => void;
 20		onEditWithReplacement?: (
 21			message: DatabaseMessage,
 22			newContent: string,
 23			shouldBranch: boolean
 24		) => void;
 25		onEditUserMessagePreserveResponses?: (
 26			message: DatabaseMessage,
 27			newContent: string,
 28			newExtras?: DatabaseMessageExtra[]
 29		) => void;
 30		onNavigateToSibling?: (siblingId: string) => void;
 31		onRegenerateWithBranching?: (message: DatabaseMessage, modelOverride?: string) => void;
 32		siblingInfo?: ChatMessageSiblingInfo | null;
 33	}
 34
 35	let {
 36		class: className = '',
 37		message,
 38		onCopy,
 39		onContinueAssistantMessage,
 40		onDelete,
 41		onEditWithBranching,
 42		onEditWithReplacement,
 43		onEditUserMessagePreserveResponses,
 44		onNavigateToSibling,
 45		onRegenerateWithBranching,
 46		siblingInfo = null
 47	}: Props = $props();
 48
 49	let deletionInfo = $state<{
 50		totalCount: number;
 51		userMessages: number;
 52		assistantMessages: number;
 53		messageTypes: string[];
 54	} | null>(null);
 55	let editedContent = $state(message.content);
 56	let editedExtras = $state<DatabaseMessageExtra[]>(message.extra ? [...message.extra] : []);
 57	let editedUploadedFiles = $state<ChatUploadedFile[]>([]);
 58	let isEditing = $state(false);
 59	let showDeleteDialog = $state(false);
 60	let shouldBranchAfterEdit = $state(false);
 61	let textareaElement: HTMLTextAreaElement | undefined = $state();
 62
 63	let thinkingContent = $derived.by(() => {
 64		if (message.role === 'assistant') {
 65			const trimmedThinking = message.thinking?.trim();
 66
 67			return trimmedThinking ? trimmedThinking : null;
 68		}
 69		return null;
 70	});
 71
 72	let toolCallContent = $derived.by((): ApiChatCompletionToolCall[] | string | null => {
 73		if (message.role === 'assistant') {
 74			const trimmedToolCalls = message.toolCalls?.trim();
 75
 76			if (!trimmedToolCalls) {
 77				return null;
 78			}
 79
 80			try {
 81				const parsed = JSON.parse(trimmedToolCalls);
 82
 83				if (Array.isArray(parsed)) {
 84					return parsed as ApiChatCompletionToolCall[];
 85				}
 86			} catch {
 87				// Harmony-only path: fall back to the raw string so issues surface visibly.
 88			}
 89
 90			return trimmedToolCalls;
 91		}
 92		return null;
 93	});
 94
 95	function handleCancelEdit() {
 96		isEditing = false;
 97		editedContent = message.content;
 98		editedExtras = message.extra ? [...message.extra] : [];
 99		editedUploadedFiles = [];
100	}
101
102	function handleEditedExtrasChange(extras: DatabaseMessageExtra[]) {
103		editedExtras = extras;
104	}
105
106	function handleEditedUploadedFilesChange(files: ChatUploadedFile[]) {
107		editedUploadedFiles = files;
108	}
109
110	async function handleCopy() {
111		const asPlainText = Boolean(config().copyTextAttachmentsAsPlainText);
112		const clipboardContent = formatMessageForClipboard(message.content, message.extra, asPlainText);
113		await copyToClipboard(clipboardContent, 'Message copied to clipboard');
114		onCopy?.(message);
115	}
116
117	function handleConfirmDelete() {
118		onDelete?.(message);
119		showDeleteDialog = false;
120	}
121
122	async function handleDelete() {
123		deletionInfo = await chatStore.getDeletionInfo(message.id);
124		showDeleteDialog = true;
125	}
126
127	function handleEdit() {
128		isEditing = true;
129		editedContent = message.content;
130		editedExtras = message.extra ? [...message.extra] : [];
131		editedUploadedFiles = [];
132
133		setTimeout(() => {
134			if (textareaElement) {
135				textareaElement.focus();
136				textareaElement.setSelectionRange(
137					textareaElement.value.length,
138					textareaElement.value.length
139				);
140			}
141		}, 0);
142	}
143
144	function handleEditedContentChange(content: string) {
145		editedContent = content;
146	}
147
148	function handleEditKeydown(event: KeyboardEvent) {
149		// Check for IME composition using isComposing property and keyCode 229 (specifically for IME composition on Safari)
150		// This prevents saving edit when confirming IME word selection (e.g., Japanese/Chinese input)
151		if (event.key === 'Enter' && !event.shiftKey && !isIMEComposing(event)) {
152			event.preventDefault();
153			handleSaveEdit();
154		} else if (event.key === 'Escape') {
155			event.preventDefault();
156			handleCancelEdit();
157		}
158	}
159
160	function handleRegenerate(modelOverride?: string) {
161		onRegenerateWithBranching?.(message, modelOverride);
162	}
163
164	function handleContinue() {
165		onContinueAssistantMessage?.(message);
166	}
167
168	async function handleSaveEdit() {
169		if (message.role === 'user' || message.role === 'system') {
170			const finalExtras = await getMergedExtras();
171			onEditWithBranching?.(message, editedContent.trim(), finalExtras);
172		} else {
173			// For assistant messages, preserve exact content including trailing whitespace
174			// This is important for the Continue feature to work properly
175			onEditWithReplacement?.(message, editedContent, shouldBranchAfterEdit);
176		}
177
178		isEditing = false;
179		shouldBranchAfterEdit = false;
180		editedUploadedFiles = [];
181	}
182
183	async function handleSaveEditOnly() {
184		if (message.role === 'user') {
185			// For user messages, trim to avoid accidental whitespace
186			const finalExtras = await getMergedExtras();
187			onEditUserMessagePreserveResponses?.(message, editedContent.trim(), finalExtras);
188		}
189
190		isEditing = false;
191		editedUploadedFiles = [];
192	}
193
194	async function getMergedExtras(): Promise<DatabaseMessageExtra[]> {
195		if (editedUploadedFiles.length === 0) {
196			return editedExtras;
197		}
198
199		const { parseFilesToMessageExtras } = await import('$lib/utils/browser-only');
200		const result = await parseFilesToMessageExtras(editedUploadedFiles);
201		const newExtras = result?.extras || [];
202
203		return [...editedExtras, ...newExtras];
204	}
205
206	function handleShowDeleteDialogChange(show: boolean) {
207		showDeleteDialog = show;
208	}
209</script>
210
211{#if message.role === 'system'}
212	<ChatMessageSystem
213		bind:textareaElement
214		class={className}
215		{deletionInfo}
216		{editedContent}
217		{isEditing}
218		{message}
219		onCancelEdit={handleCancelEdit}
220		onConfirmDelete={handleConfirmDelete}
221		onCopy={handleCopy}
222		onDelete={handleDelete}
223		onEdit={handleEdit}
224		onEditKeydown={handleEditKeydown}
225		onEditedContentChange={handleEditedContentChange}
226		{onNavigateToSibling}
227		onSaveEdit={handleSaveEdit}
228		onShowDeleteDialogChange={handleShowDeleteDialogChange}
229		{showDeleteDialog}
230		{siblingInfo}
231	/>
232{:else if message.role === 'user'}
233	<ChatMessageUser
234		bind:textareaElement
235		class={className}
236		{deletionInfo}
237		{editedContent}
238		{editedExtras}
239		{editedUploadedFiles}
240		{isEditing}
241		{message}
242		onCancelEdit={handleCancelEdit}
243		onConfirmDelete={handleConfirmDelete}
244		onCopy={handleCopy}
245		onDelete={handleDelete}
246		onEdit={handleEdit}
247		onEditKeydown={handleEditKeydown}
248		onEditedContentChange={handleEditedContentChange}
249		onEditedExtrasChange={handleEditedExtrasChange}
250		onEditedUploadedFilesChange={handleEditedUploadedFilesChange}
251		{onNavigateToSibling}
252		onSaveEdit={handleSaveEdit}
253		onSaveEditOnly={handleSaveEditOnly}
254		onShowDeleteDialogChange={handleShowDeleteDialogChange}
255		{showDeleteDialog}
256		{siblingInfo}
257	/>
258{:else}
259	<ChatMessageAssistant
260		bind:textareaElement
261		class={className}
262		{deletionInfo}
263		{editedContent}
264		{isEditing}
265		{message}
266		messageContent={message.content}
267		onCancelEdit={handleCancelEdit}
268		onConfirmDelete={handleConfirmDelete}
269		onContinue={handleContinue}
270		onCopy={handleCopy}
271		onDelete={handleDelete}
272		onEdit={handleEdit}
273		onEditKeydown={handleEditKeydown}
274		onEditedContentChange={handleEditedContentChange}
275		{onNavigateToSibling}
276		onRegenerate={handleRegenerate}
277		onSaveEdit={handleSaveEdit}
278		onShowDeleteDialogChange={handleShowDeleteDialogChange}
279		{shouldBranchAfterEdit}
280		onShouldBranchAfterEditChange={(value) => (shouldBranchAfterEdit = value)}
281		{showDeleteDialog}
282		{siblingInfo}
283		{thinkingContent}
284		{toolCallContent}
285	/>
286{/if}