refactor: Centralize chat-wide actions in ChatMessages.svelte
This commit is contained in:
parent
6b6ebd6bca
commit
cbcd7956c8
|
|
@ -1,13 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
|
import { getChatActionsContext, setMessageEditContext } from '$lib/contexts';
|
||||||
import { chatStore, pendingEditMessageId } from '$lib/stores/chat.svelte';
|
import { chatStore, pendingEditMessageId } from '$lib/stores/chat.svelte';
|
||||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||||
import { DatabaseService } from '$lib/services';
|
import { DatabaseService } from '$lib/services';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
|
||||||
import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants/ui';
|
import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants/ui';
|
||||||
import { MessageRole, AttachmentType } from '$lib/enums';
|
import { MessageRole, AttachmentType } from '$lib/enums';
|
||||||
import { copyToClipboard, isIMEComposing, formatMessageForClipboard } from '$lib/utils';
|
|
||||||
import ChatMessageAssistant from './ChatMessageAssistant.svelte';
|
import ChatMessageAssistant from './ChatMessageAssistant.svelte';
|
||||||
import ChatMessageUser from './ChatMessageUser.svelte';
|
import ChatMessageUser from './ChatMessageUser.svelte';
|
||||||
import ChatMessageSystem from './ChatMessageSystem.svelte';
|
import ChatMessageSystem from './ChatMessageSystem.svelte';
|
||||||
|
|
@ -18,42 +17,12 @@
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
message: DatabaseMessage;
|
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;
|
siblingInfo?: ChatMessageSiblingInfo | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { class: className = '', message, siblingInfo = null }: Props = $props();
|
||||||
class: className = '',
|
|
||||||
message,
|
const chatActions = getChatActionsContext();
|
||||||
onCopy,
|
|
||||||
onContinueAssistantMessage,
|
|
||||||
onDelete,
|
|
||||||
onEditWithBranching,
|
|
||||||
onEditWithReplacement,
|
|
||||||
onEditUserMessagePreserveResponses,
|
|
||||||
onNavigateToSibling,
|
|
||||||
onRegenerateWithBranching,
|
|
||||||
siblingInfo = null
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let deletionInfo = $state<{
|
let deletionInfo = $state<{
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
|
|
@ -83,7 +52,6 @@
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-start edit mode if this message is the pending edit target
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const pendingId = pendingEditMessageId();
|
const pendingId = pendingEditMessageId();
|
||||||
|
|
||||||
|
|
@ -112,23 +80,12 @@
|
||||||
editedUploadedFiles = [];
|
editedUploadedFiles = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditedExtrasChange(extras: DatabaseMessageExtra[]) {
|
function handleCopy() {
|
||||||
editedExtras = extras;
|
chatActions.copy(message);
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
function handleConfirmDelete() {
|
||||||
onDelete?.(message);
|
chatActions.delete(message);
|
||||||
showDeleteDialog = false;
|
showDeleteDialog = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,28 +116,16 @@
|
||||||
}, 0);
|
}, 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) {
|
function handleRegenerate(modelOverride?: string) {
|
||||||
onRegenerateWithBranching?.(message, modelOverride);
|
chatActions.regenerateWithBranching(message, modelOverride);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContinue() {
|
function handleContinue() {
|
||||||
onContinueAssistantMessage?.(message);
|
chatActions.continueAssistantMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNavigateToSibling(siblingId: string) {
|
||||||
|
chatActions.navigateToSibling(siblingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveEdit() {
|
async function handleSaveEdit() {
|
||||||
|
|
@ -205,11 +150,11 @@
|
||||||
}
|
}
|
||||||
} else if (message.role === MessageRole.USER) {
|
} else if (message.role === MessageRole.USER) {
|
||||||
const finalExtras = await getMergedExtras();
|
const finalExtras = await getMergedExtras();
|
||||||
onEditWithBranching?.(message, editedContent.trim(), finalExtras);
|
chatActions.editWithBranching(message, editedContent.trim(), finalExtras);
|
||||||
} else {
|
} else {
|
||||||
// For assistant messages, preserve exact content including trailing whitespace
|
// For assistant messages, preserve exact content including trailing whitespace
|
||||||
// This is important for the Continue feature to work properly
|
// This is important for the Continue feature to work properly
|
||||||
onEditWithReplacement?.(message, editedContent, shouldBranchAfterEdit);
|
chatActions.editWithReplacement(message, editedContent, shouldBranchAfterEdit);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEditing = false;
|
isEditing = false;
|
||||||
|
|
@ -250,18 +195,12 @@
|
||||||
bind:textareaElement
|
bind:textareaElement
|
||||||
class={className}
|
class={className}
|
||||||
{deletionInfo}
|
{deletionInfo}
|
||||||
{editedContent}
|
|
||||||
{isEditing}
|
|
||||||
{message}
|
{message}
|
||||||
onCancelEdit={handleCancelEdit}
|
|
||||||
onConfirmDelete={handleConfirmDelete}
|
onConfirmDelete={handleConfirmDelete}
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onEditKeydown={handleEditKeydown}
|
onNavigateToSibling={handleNavigateToSibling}
|
||||||
onEditedContentChange={handleEditedContentChange}
|
|
||||||
{onNavigateToSibling}
|
|
||||||
onSaveEdit={handleSaveEdit}
|
|
||||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
||||||
{showDeleteDialog}
|
{showDeleteDialog}
|
||||||
{siblingInfo}
|
{siblingInfo}
|
||||||
|
|
@ -270,23 +209,13 @@
|
||||||
<ChatMessageMcpPrompt
|
<ChatMessageMcpPrompt
|
||||||
class={className}
|
class={className}
|
||||||
{deletionInfo}
|
{deletionInfo}
|
||||||
{editedContent}
|
|
||||||
{editedExtras}
|
|
||||||
{editedUploadedFiles}
|
|
||||||
{isEditing}
|
|
||||||
{message}
|
{message}
|
||||||
mcpPrompt={mcpPromptExtra}
|
mcpPrompt={mcpPromptExtra}
|
||||||
onCancelEdit={handleCancelEdit}
|
|
||||||
onConfirmDelete={handleConfirmDelete}
|
onConfirmDelete={handleConfirmDelete}
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onEditedContentChange={handleEditedContentChange}
|
onNavigateToSibling={handleNavigateToSibling}
|
||||||
onEditedExtrasChange={handleEditedExtrasChange}
|
|
||||||
onEditedUploadedFilesChange={handleEditedUploadedFilesChange}
|
|
||||||
{onNavigateToSibling}
|
|
||||||
onSaveEdit={handleSaveEdit}
|
|
||||||
onSaveEditOnly={handleSaveEditOnly}
|
|
||||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
||||||
{showDeleteDialog}
|
{showDeleteDialog}
|
||||||
{siblingInfo}
|
{siblingInfo}
|
||||||
|
|
@ -295,22 +224,12 @@
|
||||||
<ChatMessageUser
|
<ChatMessageUser
|
||||||
class={className}
|
class={className}
|
||||||
{deletionInfo}
|
{deletionInfo}
|
||||||
{editedContent}
|
|
||||||
{editedExtras}
|
|
||||||
{editedUploadedFiles}
|
|
||||||
{isEditing}
|
|
||||||
{message}
|
{message}
|
||||||
onCancelEdit={handleCancelEdit}
|
|
||||||
onConfirmDelete={handleConfirmDelete}
|
onConfirmDelete={handleConfirmDelete}
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onEditedContentChange={handleEditedContentChange}
|
onNavigateToSibling={handleNavigateToSibling}
|
||||||
onEditedExtrasChange={handleEditedExtrasChange}
|
|
||||||
onEditedUploadedFilesChange={handleEditedUploadedFilesChange}
|
|
||||||
{onNavigateToSibling}
|
|
||||||
onSaveEdit={handleSaveEdit}
|
|
||||||
onSaveEditOnly={handleSaveEditOnly}
|
|
||||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
||||||
{showDeleteDialog}
|
{showDeleteDialog}
|
||||||
{siblingInfo}
|
{siblingInfo}
|
||||||
|
|
@ -320,24 +239,16 @@
|
||||||
bind:textareaElement
|
bind:textareaElement
|
||||||
class={className}
|
class={className}
|
||||||
{deletionInfo}
|
{deletionInfo}
|
||||||
{editedContent}
|
|
||||||
{isEditing}
|
|
||||||
{message}
|
{message}
|
||||||
messageContent={message.content}
|
messageContent={message.content}
|
||||||
onCancelEdit={handleCancelEdit}
|
|
||||||
onConfirmDelete={handleConfirmDelete}
|
onConfirmDelete={handleConfirmDelete}
|
||||||
onContinue={handleContinue}
|
onContinue={handleContinue}
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onEditKeydown={handleEditKeydown}
|
onNavigateToSibling={handleNavigateToSibling}
|
||||||
onEditedContentChange={handleEditedContentChange}
|
|
||||||
{onNavigateToSibling}
|
|
||||||
onRegenerate={handleRegenerate}
|
onRegenerate={handleRegenerate}
|
||||||
onSaveEdit={handleSaveEdit}
|
|
||||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
||||||
{shouldBranchAfterEdit}
|
|
||||||
onShouldBranchAfterEditChange={(value) => (shouldBranchAfterEdit = value)}
|
|
||||||
{showDeleteDialog}
|
{showDeleteDialog}
|
||||||
{siblingInfo}
|
{siblingInfo}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ChatMessage } from '$lib/components/app';
|
import { ChatMessage } from '$lib/components/app';
|
||||||
|
import { setChatActionsContext } from '$lib/contexts';
|
||||||
import { MessageRole } from '$lib/enums';
|
import { MessageRole } from '$lib/enums';
|
||||||
import { chatStore } from '$lib/stores/chat.svelte';
|
import { chatStore } from '$lib/stores/chat.svelte';
|
||||||
import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte';
|
import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { getMessageSiblings } from '$lib/utils';
|
import { copyToClipboard, formatMessageForClipboard, getMessageSiblings } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|
@ -17,6 +18,62 @@
|
||||||
let allConversationMessages = $state<DatabaseMessage[]>([]);
|
let allConversationMessages = $state<DatabaseMessage[]>([]);
|
||||||
const currentConfig = config();
|
const currentConfig = config();
|
||||||
|
|
||||||
|
setChatActionsContext({
|
||||||
|
copy: async (message: DatabaseMessage) => {
|
||||||
|
const asPlainText = Boolean(currentConfig.copyTextAttachmentsAsPlainText);
|
||||||
|
const clipboardContent = formatMessageForClipboard(
|
||||||
|
message.content,
|
||||||
|
message.extra,
|
||||||
|
asPlainText
|
||||||
|
);
|
||||||
|
await copyToClipboard(clipboardContent, 'Message copied to clipboard');
|
||||||
|
},
|
||||||
|
delete: async (message: DatabaseMessage) => {
|
||||||
|
await chatStore.deleteMessage(message.id);
|
||||||
|
refreshAllMessages();
|
||||||
|
},
|
||||||
|
navigateToSibling: async (siblingId: string) => {
|
||||||
|
await conversationsStore.navigateToSibling(siblingId);
|
||||||
|
},
|
||||||
|
editWithBranching: async (
|
||||||
|
message: DatabaseMessage,
|
||||||
|
newContent: string,
|
||||||
|
newExtras?: DatabaseMessageExtra[]
|
||||||
|
) => {
|
||||||
|
onUserAction?.();
|
||||||
|
await chatStore.editMessageWithBranching(message.id, newContent, newExtras);
|
||||||
|
refreshAllMessages();
|
||||||
|
},
|
||||||
|
editWithReplacement: async (
|
||||||
|
message: DatabaseMessage,
|
||||||
|
newContent: string,
|
||||||
|
shouldBranch: boolean
|
||||||
|
) => {
|
||||||
|
onUserAction?.();
|
||||||
|
await chatStore.editAssistantMessage(message.id, newContent, shouldBranch);
|
||||||
|
refreshAllMessages();
|
||||||
|
},
|
||||||
|
editUserMessagePreserveResponses: async (
|
||||||
|
message: DatabaseMessage,
|
||||||
|
newContent: string,
|
||||||
|
newExtras?: DatabaseMessageExtra[]
|
||||||
|
) => {
|
||||||
|
onUserAction?.();
|
||||||
|
await chatStore.editUserMessagePreserveResponses(message.id, newContent, newExtras);
|
||||||
|
refreshAllMessages();
|
||||||
|
},
|
||||||
|
regenerateWithBranching: async (message: DatabaseMessage, modelOverride?: string) => {
|
||||||
|
onUserAction?.();
|
||||||
|
await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
|
||||||
|
refreshAllMessages();
|
||||||
|
},
|
||||||
|
continueAssistantMessage: async (message: DatabaseMessage) => {
|
||||||
|
onUserAction?.();
|
||||||
|
await chatStore.continueAssistantMessage(message.id);
|
||||||
|
refreshAllMessages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function refreshAllMessages() {
|
function refreshAllMessages() {
|
||||||
const conversation = activeConversation();
|
const conversation = activeConversation();
|
||||||
|
|
||||||
|
|
@ -62,83 +119,10 @@
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleNavigateToSibling(siblingId: string) {
|
|
||||||
await conversationsStore.navigateToSibling(siblingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleEditWithBranching(
|
|
||||||
message: DatabaseMessage,
|
|
||||||
newContent: string,
|
|
||||||
newExtras?: DatabaseMessageExtra[]
|
|
||||||
) {
|
|
||||||
onUserAction?.();
|
|
||||||
|
|
||||||
await chatStore.editMessageWithBranching(message.id, newContent, newExtras);
|
|
||||||
|
|
||||||
refreshAllMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleEditWithReplacement(
|
|
||||||
message: DatabaseMessage,
|
|
||||||
newContent: string,
|
|
||||||
shouldBranch: boolean
|
|
||||||
) {
|
|
||||||
onUserAction?.();
|
|
||||||
|
|
||||||
await chatStore.editAssistantMessage(message.id, newContent, shouldBranch);
|
|
||||||
|
|
||||||
refreshAllMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRegenerateWithBranching(message: DatabaseMessage, modelOverride?: string) {
|
|
||||||
onUserAction?.();
|
|
||||||
|
|
||||||
await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
|
|
||||||
|
|
||||||
refreshAllMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleContinueAssistantMessage(message: DatabaseMessage) {
|
|
||||||
onUserAction?.();
|
|
||||||
|
|
||||||
await chatStore.continueAssistantMessage(message.id);
|
|
||||||
|
|
||||||
refreshAllMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleEditUserMessagePreserveResponses(
|
|
||||||
message: DatabaseMessage,
|
|
||||||
newContent: string,
|
|
||||||
newExtras?: DatabaseMessageExtra[]
|
|
||||||
) {
|
|
||||||
onUserAction?.();
|
|
||||||
|
|
||||||
await chatStore.editUserMessagePreserveResponses(message.id, newContent, newExtras);
|
|
||||||
|
|
||||||
refreshAllMessages();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDeleteMessage(message: DatabaseMessage) {
|
|
||||||
await chatStore.deleteMessage(message.id);
|
|
||||||
|
|
||||||
refreshAllMessages();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full flex-col space-y-10 pt-16 md:pt-24 {className}" style="height: auto; ">
|
<div class="flex h-full flex-col space-y-10 pt-16 md:pt-24 {className}" style="height: auto; ">
|
||||||
{#each displayMessages as { message, siblingInfo } (message.id)}
|
{#each displayMessages as { message, siblingInfo } (message.id)}
|
||||||
<ChatMessage
|
<ChatMessage class="mx-auto w-full max-w-[48rem]" {message} {siblingInfo} />
|
||||||
class="mx-auto w-full max-w-[48rem]"
|
|
||||||
{message}
|
|
||||||
{siblingInfo}
|
|
||||||
onDelete={handleDeleteMessage}
|
|
||||||
onNavigateToSibling={handleNavigateToSibling}
|
|
||||||
onEditWithBranching={handleEditWithBranching}
|
|
||||||
onEditWithReplacement={handleEditWithReplacement}
|
|
||||||
onEditUserMessagePreserveResponses={handleEditUserMessagePreserveResponses}
|
|
||||||
onRegenerateWithBranching={handleRegenerateWithBranching}
|
|
||||||
onContinueAssistantMessage={handleContinueAssistantMessage}
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue