ChatMessageUser.svelte 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. <script lang="ts">
  2. import { Check, X, Send } from '@lucide/svelte';
  3. import { Card } from '$lib/components/ui/card';
  4. import { Button } from '$lib/components/ui/button';
  5. import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
  6. import { INPUT_CLASSES } from '$lib/constants/input-classes';
  7. import { config } from '$lib/stores/settings.svelte';
  8. import autoResizeTextarea from '$lib/utils/autoresize-textarea';
  9. import ChatMessageActions from './ChatMessageActions.svelte';
  10. interface Props {
  11. class?: string;
  12. message: DatabaseMessage;
  13. isEditing: boolean;
  14. editedContent: string;
  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. onCopy: () => void;
  29. onEdit: () => void;
  30. onDelete: () => void;
  31. onConfirmDelete: () => void;
  32. onNavigateToSibling?: (siblingId: string) => void;
  33. onShowDeleteDialogChange: (show: boolean) => void;
  34. textareaElement?: HTMLTextAreaElement;
  35. }
  36. let {
  37. class: className = '',
  38. message,
  39. isEditing,
  40. editedContent,
  41. siblingInfo = null,
  42. showDeleteDialog,
  43. deletionInfo,
  44. onCancelEdit,
  45. onSaveEdit,
  46. onSaveEditOnly,
  47. onEditKeydown,
  48. onEditedContentChange,
  49. onCopy,
  50. onEdit,
  51. onDelete,
  52. onConfirmDelete,
  53. onNavigateToSibling,
  54. onShowDeleteDialogChange,
  55. textareaElement = $bindable()
  56. }: Props = $props();
  57. let isMultiline = $state(false);
  58. let messageElement: HTMLElement | undefined = $state();
  59. const currentConfig = config();
  60. $effect(() => {
  61. if (isEditing && textareaElement) {
  62. autoResizeTextarea(textareaElement);
  63. }
  64. });
  65. $effect(() => {
  66. if (!messageElement || !message.content.trim()) return;
  67. if (message.content.includes('\n')) {
  68. isMultiline = true;
  69. return;
  70. }
  71. const resizeObserver = new ResizeObserver((entries) => {
  72. for (const entry of entries) {
  73. const element = entry.target as HTMLElement;
  74. const estimatedSingleLineHeight = 24; // Typical line height for text-md
  75. isMultiline = element.offsetHeight > estimatedSingleLineHeight * 1.5;
  76. }
  77. });
  78. resizeObserver.observe(messageElement);
  79. return () => {
  80. resizeObserver.disconnect();
  81. };
  82. });
  83. </script>
  84. <div
  85. aria-label="User message with actions"
  86. class="group flex flex-col items-end gap-3 md:gap-2 {className}"
  87. role="group"
  88. >
  89. {#if isEditing}
  90. <div class="w-full max-w-[80%]">
  91. <textarea
  92. bind:this={textareaElement}
  93. bind:value={editedContent}
  94. class="min-h-[60px] w-full resize-none rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
  95. onkeydown={onEditKeydown}
  96. oninput={(e) => {
  97. autoResizeTextarea(e.currentTarget);
  98. onEditedContentChange(e.currentTarget.value);
  99. }}
  100. placeholder="Edit your message..."
  101. ></textarea>
  102. <div class="mt-2 flex justify-end gap-2">
  103. <Button class="h-8 px-3" onclick={onCancelEdit} size="sm" variant="ghost">
  104. <X class="mr-1 h-3 w-3" />
  105. Cancel
  106. </Button>
  107. {#if onSaveEditOnly}
  108. <Button
  109. class="h-8 px-3"
  110. onclick={onSaveEditOnly}
  111. disabled={!editedContent.trim()}
  112. size="sm"
  113. variant="outline"
  114. >
  115. <Check class="mr-1 h-3 w-3" />
  116. Save
  117. </Button>
  118. {/if}
  119. <Button class="h-8 px-3" onclick={onSaveEdit} disabled={!editedContent.trim()} size="sm">
  120. <Send class="mr-1 h-3 w-3" />
  121. Send
  122. </Button>
  123. </div>
  124. </div>
  125. {:else}
  126. {#if message.extra && message.extra.length > 0}
  127. <div class="mb-2 max-w-[80%]">
  128. <ChatAttachmentsList attachments={message.extra} readonly={true} imageHeight="h-80" />
  129. </div>
  130. {/if}
  131. {#if message.content.trim()}
  132. <Card
  133. class="max-w-[80%] rounded-[1.125rem] bg-primary px-3.75 py-1.5 text-primary-foreground data-[multiline]:py-2.5"
  134. data-multiline={isMultiline ? '' : undefined}
  135. >
  136. {#if currentConfig.renderUserContentAsMarkdown}
  137. <div bind:this={messageElement} class="text-md">
  138. <MarkdownContent
  139. class="markdown-user-content text-primary-foreground"
  140. content={message.content}
  141. />
  142. </div>
  143. {:else}
  144. <span bind:this={messageElement} class="text-md whitespace-pre-wrap">
  145. {message.content}
  146. </span>
  147. {/if}
  148. </Card>
  149. {/if}
  150. {#if message.timestamp}
  151. <div class="max-w-[80%]">
  152. <ChatMessageActions
  153. actionsPosition="right"
  154. {deletionInfo}
  155. justify="end"
  156. {onConfirmDelete}
  157. {onCopy}
  158. {onDelete}
  159. {onEdit}
  160. {onNavigateToSibling}
  161. {onShowDeleteDialogChange}
  162. {siblingInfo}
  163. {showDeleteDialog}
  164. role="user"
  165. />
  166. </div>
  167. {/if}
  168. {/if}
  169. </div>