ChatMessage.svelte 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <script lang="ts">
  2. import { getDeletionInfo } from '$lib/stores/chat.svelte';
  3. import { copyToClipboard } from '$lib/utils/copy';
  4. import { isIMEComposing } from '$lib/utils/is-ime-composing';
  5. import type { ApiChatCompletionToolCall } from '$lib/types/api';
  6. import ChatMessageAssistant from './ChatMessageAssistant.svelte';
  7. import ChatMessageUser from './ChatMessageUser.svelte';
  8. interface Props {
  9. class?: string;
  10. message: DatabaseMessage;
  11. onCopy?: (message: DatabaseMessage) => void;
  12. onDelete?: (message: DatabaseMessage) => void;
  13. onEditWithBranching?: (message: DatabaseMessage, newContent: string) => void;
  14. onEditWithReplacement?: (
  15. message: DatabaseMessage,
  16. newContent: string,
  17. shouldBranch: boolean
  18. ) => void;
  19. onNavigateToSibling?: (siblingId: string) => void;
  20. onRegenerateWithBranching?: (message: DatabaseMessage) => void;
  21. siblingInfo?: ChatMessageSiblingInfo | null;
  22. }
  23. let {
  24. class: className = '',
  25. message,
  26. onCopy,
  27. onDelete,
  28. onEditWithBranching,
  29. onEditWithReplacement,
  30. onNavigateToSibling,
  31. onRegenerateWithBranching,
  32. siblingInfo = null
  33. }: Props = $props();
  34. let deletionInfo = $state<{
  35. totalCount: number;
  36. userMessages: number;
  37. assistantMessages: number;
  38. messageTypes: string[];
  39. } | null>(null);
  40. let editedContent = $state(message.content);
  41. let isEditing = $state(false);
  42. let showDeleteDialog = $state(false);
  43. let shouldBranchAfterEdit = $state(false);
  44. let textareaElement: HTMLTextAreaElement | undefined = $state();
  45. let thinkingContent = $derived.by(() => {
  46. if (message.role === 'assistant') {
  47. const trimmedThinking = message.thinking?.trim();
  48. return trimmedThinking ? trimmedThinking : null;
  49. }
  50. return null;
  51. });
  52. let toolCallContent = $derived.by((): ApiChatCompletionToolCall[] | string | null => {
  53. if (message.role === 'assistant') {
  54. const trimmedToolCalls = message.toolCalls?.trim();
  55. if (!trimmedToolCalls) {
  56. return null;
  57. }
  58. try {
  59. const parsed = JSON.parse(trimmedToolCalls);
  60. if (Array.isArray(parsed)) {
  61. return parsed as ApiChatCompletionToolCall[];
  62. }
  63. } catch {
  64. // Harmony-only path: fall back to the raw string so issues surface visibly.
  65. }
  66. return trimmedToolCalls;
  67. }
  68. return null;
  69. });
  70. function handleCancelEdit() {
  71. isEditing = false;
  72. editedContent = message.content;
  73. }
  74. async function handleCopy() {
  75. await copyToClipboard(message.content, 'Message copied to clipboard');
  76. onCopy?.(message);
  77. }
  78. function handleConfirmDelete() {
  79. onDelete?.(message);
  80. showDeleteDialog = false;
  81. }
  82. async function handleDelete() {
  83. deletionInfo = await getDeletionInfo(message.id);
  84. showDeleteDialog = true;
  85. }
  86. function handleEdit() {
  87. isEditing = true;
  88. editedContent = message.content;
  89. setTimeout(() => {
  90. if (textareaElement) {
  91. textareaElement.focus();
  92. textareaElement.setSelectionRange(
  93. textareaElement.value.length,
  94. textareaElement.value.length
  95. );
  96. }
  97. }, 0);
  98. }
  99. function handleEditedContentChange(content: string) {
  100. editedContent = content;
  101. }
  102. function handleEditKeydown(event: KeyboardEvent) {
  103. // Check for IME composition using isComposing property and keyCode 229 (specifically for IME composition on Safari)
  104. // This prevents saving edit when confirming IME word selection (e.g., Japanese/Chinese input)
  105. if (event.key === 'Enter' && !event.shiftKey && !isIMEComposing(event)) {
  106. event.preventDefault();
  107. handleSaveEdit();
  108. } else if (event.key === 'Escape') {
  109. event.preventDefault();
  110. handleCancelEdit();
  111. }
  112. }
  113. function handleRegenerate() {
  114. onRegenerateWithBranching?.(message);
  115. }
  116. function handleSaveEdit() {
  117. if (message.role === 'user') {
  118. onEditWithBranching?.(message, editedContent.trim());
  119. } else {
  120. onEditWithReplacement?.(message, editedContent.trim(), shouldBranchAfterEdit);
  121. }
  122. isEditing = false;
  123. shouldBranchAfterEdit = false;
  124. }
  125. function handleShowDeleteDialogChange(show: boolean) {
  126. showDeleteDialog = show;
  127. }
  128. </script>
  129. {#if message.role === 'user'}
  130. <ChatMessageUser
  131. bind:textareaElement
  132. class={className}
  133. {deletionInfo}
  134. {editedContent}
  135. {isEditing}
  136. {message}
  137. onCancelEdit={handleCancelEdit}
  138. onConfirmDelete={handleConfirmDelete}
  139. onCopy={handleCopy}
  140. onDelete={handleDelete}
  141. onEdit={handleEdit}
  142. onEditKeydown={handleEditKeydown}
  143. onEditedContentChange={handleEditedContentChange}
  144. {onNavigateToSibling}
  145. onSaveEdit={handleSaveEdit}
  146. onShowDeleteDialogChange={handleShowDeleteDialogChange}
  147. {showDeleteDialog}
  148. {siblingInfo}
  149. />
  150. {:else}
  151. <ChatMessageAssistant
  152. bind:textareaElement
  153. class={className}
  154. {deletionInfo}
  155. {editedContent}
  156. {isEditing}
  157. {message}
  158. messageContent={message.content}
  159. onCancelEdit={handleCancelEdit}
  160. onConfirmDelete={handleConfirmDelete}
  161. onCopy={handleCopy}
  162. onDelete={handleDelete}
  163. onEdit={handleEdit}
  164. onEditKeydown={handleEditKeydown}
  165. onEditedContentChange={handleEditedContentChange}
  166. {onNavigateToSibling}
  167. onRegenerate={handleRegenerate}
  168. onSaveEdit={handleSaveEdit}
  169. onShowDeleteDialogChange={handleShowDeleteDialogChange}
  170. {shouldBranchAfterEdit}
  171. onShouldBranchAfterEditChange={(value) => (shouldBranchAfterEdit = value)}
  172. {showDeleteDialog}
  173. {siblingInfo}
  174. {thinkingContent}
  175. {toolCallContent}
  176. />
  177. {/if}