| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- <script lang="ts">
- import { chatStore } from '$lib/stores/chat.svelte';
- import { config } from '$lib/stores/settings.svelte';
- import { copyToClipboard, isIMEComposing, formatMessageForClipboard } from '$lib/utils';
- import ChatMessageAssistant from './ChatMessageAssistant.svelte';
- import ChatMessageUser from './ChatMessageUser.svelte';
- import ChatMessageSystem from './ChatMessageSystem.svelte';
- interface Props {
- class?: string;
- message: DatabaseMessage;
- onCopy?: (message: DatabaseMessage) => void;
- onContinueAssistantMessage?: (message: DatabaseMessage) => void;
- onDelete?: (message: DatabaseMessage) => void;
- onEditWithBranching?: (
- message: DatabaseMessage,
- newContent: string,
- newExtras?: DatabaseMessageExtra[]
- ) => void;
- onEditWithReplacement?: (
- message: DatabaseMessage,
- newContent: string,
- shouldBranch: boolean
- ) => void;
- onEditUserMessagePreserveResponses?: (
- message: DatabaseMessage,
- newContent: string,
- newExtras?: DatabaseMessageExtra[]
- ) => void;
- onNavigateToSibling?: (siblingId: string) => void;
- onRegenerateWithBranching?: (message: DatabaseMessage, modelOverride?: string) => void;
- siblingInfo?: ChatMessageSiblingInfo | null;
- }
- let {
- class: className = '',
- message,
- onCopy,
- onContinueAssistantMessage,
- onDelete,
- onEditWithBranching,
- onEditWithReplacement,
- onEditUserMessagePreserveResponses,
- onNavigateToSibling,
- onRegenerateWithBranching,
- siblingInfo = null
- }: Props = $props();
- let deletionInfo = $state<{
- totalCount: number;
- userMessages: number;
- assistantMessages: number;
- messageTypes: string[];
- } | null>(null);
- let editedContent = $state(message.content);
- let editedExtras = $state<DatabaseMessageExtra[]>(message.extra ? [...message.extra] : []);
- let editedUploadedFiles = $state<ChatUploadedFile[]>([]);
- let isEditing = $state(false);
- let showDeleteDialog = $state(false);
- let shouldBranchAfterEdit = $state(false);
- let textareaElement: HTMLTextAreaElement | undefined = $state();
- let thinkingContent = $derived.by(() => {
- if (message.role === 'assistant') {
- const trimmedThinking = message.thinking?.trim();
- return trimmedThinking ? trimmedThinking : null;
- }
- return null;
- });
- let toolCallContent = $derived.by((): ApiChatCompletionToolCall[] | string | null => {
- if (message.role === 'assistant') {
- const trimmedToolCalls = message.toolCalls?.trim();
- if (!trimmedToolCalls) {
- return null;
- }
- try {
- const parsed = JSON.parse(trimmedToolCalls);
- if (Array.isArray(parsed)) {
- return parsed as ApiChatCompletionToolCall[];
- }
- } catch {
- // Harmony-only path: fall back to the raw string so issues surface visibly.
- }
- return trimmedToolCalls;
- }
- return null;
- });
- function handleCancelEdit() {
- isEditing = false;
- editedContent = message.content;
- editedExtras = message.extra ? [...message.extra] : [];
- editedUploadedFiles = [];
- }
- function handleEditedExtrasChange(extras: DatabaseMessageExtra[]) {
- editedExtras = extras;
- }
- function handleEditedUploadedFilesChange(files: ChatUploadedFile[]) {
- editedUploadedFiles = files;
- }
- async function handleCopy() {
- const asPlainText = Boolean(config().copyTextAttachmentsAsPlainText);
- const clipboardContent = formatMessageForClipboard(message.content, message.extra, asPlainText);
- await copyToClipboard(clipboardContent, 'Message copied to clipboard');
- onCopy?.(message);
- }
- function handleConfirmDelete() {
- onDelete?.(message);
- showDeleteDialog = false;
- }
- async function handleDelete() {
- deletionInfo = await chatStore.getDeletionInfo(message.id);
- showDeleteDialog = true;
- }
- function handleEdit() {
- isEditing = true;
- editedContent = message.content;
- editedExtras = message.extra ? [...message.extra] : [];
- editedUploadedFiles = [];
- setTimeout(() => {
- if (textareaElement) {
- textareaElement.focus();
- textareaElement.setSelectionRange(
- textareaElement.value.length,
- textareaElement.value.length
- );
- }
- }, 0);
- }
- function handleEditedContentChange(content: string) {
- editedContent = content;
- }
- function handleEditKeydown(event: KeyboardEvent) {
- // Check for IME composition using isComposing property and keyCode 229 (specifically for IME composition on Safari)
- // This prevents saving edit when confirming IME word selection (e.g., Japanese/Chinese input)
- if (event.key === 'Enter' && !event.shiftKey && !isIMEComposing(event)) {
- event.preventDefault();
- handleSaveEdit();
- } else if (event.key === 'Escape') {
- event.preventDefault();
- handleCancelEdit();
- }
- }
- function handleRegenerate(modelOverride?: string) {
- onRegenerateWithBranching?.(message, modelOverride);
- }
- function handleContinue() {
- onContinueAssistantMessage?.(message);
- }
- async function handleSaveEdit() {
- if (message.role === 'user' || message.role === 'system') {
- const finalExtras = await getMergedExtras();
- onEditWithBranching?.(message, editedContent.trim(), finalExtras);
- } else {
- // For assistant messages, preserve exact content including trailing whitespace
- // This is important for the Continue feature to work properly
- onEditWithReplacement?.(message, editedContent, shouldBranchAfterEdit);
- }
- isEditing = false;
- shouldBranchAfterEdit = false;
- editedUploadedFiles = [];
- }
- async function handleSaveEditOnly() {
- if (message.role === 'user') {
- // For user messages, trim to avoid accidental whitespace
- const finalExtras = await getMergedExtras();
- onEditUserMessagePreserveResponses?.(message, editedContent.trim(), finalExtras);
- }
- isEditing = false;
- editedUploadedFiles = [];
- }
- async function getMergedExtras(): Promise<DatabaseMessageExtra[]> {
- if (editedUploadedFiles.length === 0) {
- return editedExtras;
- }
- const { parseFilesToMessageExtras } = await import('$lib/utils/browser-only');
- const result = await parseFilesToMessageExtras(editedUploadedFiles);
- const newExtras = result?.extras || [];
- return [...editedExtras, ...newExtras];
- }
- function handleShowDeleteDialogChange(show: boolean) {
- showDeleteDialog = show;
- }
- </script>
- {#if message.role === 'system'}
- <ChatMessageSystem
- bind:textareaElement
- class={className}
- {deletionInfo}
- {editedContent}
- {isEditing}
- {message}
- onCancelEdit={handleCancelEdit}
- onConfirmDelete={handleConfirmDelete}
- onCopy={handleCopy}
- onDelete={handleDelete}
- onEdit={handleEdit}
- onEditKeydown={handleEditKeydown}
- onEditedContentChange={handleEditedContentChange}
- {onNavigateToSibling}
- onSaveEdit={handleSaveEdit}
- onShowDeleteDialogChange={handleShowDeleteDialogChange}
- {showDeleteDialog}
- {siblingInfo}
- />
- {:else if message.role === 'user'}
- <ChatMessageUser
- bind:textareaElement
- class={className}
- {deletionInfo}
- {editedContent}
- {editedExtras}
- {editedUploadedFiles}
- {isEditing}
- {message}
- onCancelEdit={handleCancelEdit}
- onConfirmDelete={handleConfirmDelete}
- onCopy={handleCopy}
- onDelete={handleDelete}
- onEdit={handleEdit}
- onEditKeydown={handleEditKeydown}
- onEditedContentChange={handleEditedContentChange}
- onEditedExtrasChange={handleEditedExtrasChange}
- onEditedUploadedFilesChange={handleEditedUploadedFilesChange}
- {onNavigateToSibling}
- onSaveEdit={handleSaveEdit}
- onSaveEditOnly={handleSaveEditOnly}
- onShowDeleteDialogChange={handleShowDeleteDialogChange}
- {showDeleteDialog}
- {siblingInfo}
- />
- {:else}
- <ChatMessageAssistant
- bind:textareaElement
- class={className}
- {deletionInfo}
- {editedContent}
- {isEditing}
- {message}
- messageContent={message.content}
- onCancelEdit={handleCancelEdit}
- onConfirmDelete={handleConfirmDelete}
- onContinue={handleContinue}
- onCopy={handleCopy}
- onDelete={handleDelete}
- onEdit={handleEdit}
- onEditKeydown={handleEditKeydown}
- onEditedContentChange={handleEditedContentChange}
- {onNavigateToSibling}
- onRegenerate={handleRegenerate}
- onSaveEdit={handleSaveEdit}
- onShowDeleteDialogChange={handleShowDeleteDialogChange}
- {shouldBranchAfterEdit}
- onShouldBranchAfterEditChange={(value) => (shouldBranchAfterEdit = value)}
- {showDeleteDialog}
- {siblingInfo}
- {thinkingContent}
- {toolCallContent}
- />
- {/if}
|