| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- <script lang="ts">
- import { Check, X, Send } from '@lucide/svelte';
- import { Card } from '$lib/components/ui/card';
- import { Button } from '$lib/components/ui/button';
- import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
- import { INPUT_CLASSES } from '$lib/constants/input-classes';
- import { config } from '$lib/stores/settings.svelte';
- import autoResizeTextarea from '$lib/utils/autoresize-textarea';
- import ChatMessageActions from './ChatMessageActions.svelte';
- interface Props {
- class?: string;
- message: DatabaseMessage;
- isEditing: boolean;
- editedContent: string;
- siblingInfo?: ChatMessageSiblingInfo | null;
- showDeleteDialog: boolean;
- deletionInfo: {
- totalCount: number;
- userMessages: number;
- assistantMessages: number;
- messageTypes: string[];
- } | null;
- onCancelEdit: () => void;
- onSaveEdit: () => void;
- onSaveEditOnly?: () => void;
- onEditKeydown: (event: KeyboardEvent) => void;
- onEditedContentChange: (content: string) => void;
- onCopy: () => void;
- onEdit: () => void;
- onDelete: () => void;
- onConfirmDelete: () => void;
- onNavigateToSibling?: (siblingId: string) => void;
- onShowDeleteDialogChange: (show: boolean) => void;
- textareaElement?: HTMLTextAreaElement;
- }
- let {
- class: className = '',
- message,
- isEditing,
- editedContent,
- siblingInfo = null,
- showDeleteDialog,
- deletionInfo,
- onCancelEdit,
- onSaveEdit,
- onSaveEditOnly,
- onEditKeydown,
- onEditedContentChange,
- onCopy,
- onEdit,
- onDelete,
- onConfirmDelete,
- onNavigateToSibling,
- onShowDeleteDialogChange,
- textareaElement = $bindable()
- }: Props = $props();
- let isMultiline = $state(false);
- let messageElement: HTMLElement | undefined = $state();
- const currentConfig = config();
- $effect(() => {
- if (isEditing && textareaElement) {
- autoResizeTextarea(textareaElement);
- }
- });
- $effect(() => {
- if (!messageElement || !message.content.trim()) return;
- if (message.content.includes('\n')) {
- isMultiline = true;
- return;
- }
- const resizeObserver = new ResizeObserver((entries) => {
- for (const entry of entries) {
- const element = entry.target as HTMLElement;
- const estimatedSingleLineHeight = 24; // Typical line height for text-md
- isMultiline = element.offsetHeight > estimatedSingleLineHeight * 1.5;
- }
- });
- resizeObserver.observe(messageElement);
- return () => {
- resizeObserver.disconnect();
- };
- });
- </script>
- <div
- aria-label="User message with actions"
- class="group flex flex-col items-end gap-3 md:gap-2 {className}"
- role="group"
- >
- {#if isEditing}
- <div class="w-full max-w-[80%]">
- <textarea
- bind:this={textareaElement}
- bind:value={editedContent}
- class="min-h-[60px] w-full resize-none rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
- onkeydown={onEditKeydown}
- oninput={(e) => {
- autoResizeTextarea(e.currentTarget);
- onEditedContentChange(e.currentTarget.value);
- }}
- placeholder="Edit your message..."
- ></textarea>
- <div class="mt-2 flex justify-end gap-2">
- <Button class="h-8 px-3" onclick={onCancelEdit} size="sm" variant="ghost">
- <X class="mr-1 h-3 w-3" />
- Cancel
- </Button>
- {#if onSaveEditOnly}
- <Button
- class="h-8 px-3"
- onclick={onSaveEditOnly}
- disabled={!editedContent.trim()}
- size="sm"
- variant="outline"
- >
- <Check class="mr-1 h-3 w-3" />
- Save
- </Button>
- {/if}
- <Button class="h-8 px-3" onclick={onSaveEdit} disabled={!editedContent.trim()} size="sm">
- <Send class="mr-1 h-3 w-3" />
- Send
- </Button>
- </div>
- </div>
- {:else}
- {#if message.extra && message.extra.length > 0}
- <div class="mb-2 max-w-[80%]">
- <ChatAttachmentsList attachments={message.extra} readonly={true} imageHeight="h-80" />
- </div>
- {/if}
- {#if message.content.trim()}
- <Card
- class="max-w-[80%] rounded-[1.125rem] bg-primary px-3.75 py-1.5 text-primary-foreground data-[multiline]:py-2.5"
- data-multiline={isMultiline ? '' : undefined}
- >
- {#if currentConfig.renderUserContentAsMarkdown}
- <div bind:this={messageElement} class="text-md">
- <MarkdownContent
- class="markdown-user-content text-primary-foreground"
- content={message.content}
- />
- </div>
- {:else}
- <span bind:this={messageElement} class="text-md whitespace-pre-wrap">
- {message.content}
- </span>
- {/if}
- </Card>
- {/if}
- {#if message.timestamp}
- <div class="max-w-[80%]">
- <ChatMessageActions
- actionsPosition="right"
- {deletionInfo}
- justify="end"
- {onConfirmDelete}
- {onCopy}
- {onDelete}
- {onEdit}
- {onNavigateToSibling}
- {onShowDeleteDialogChange}
- {siblingInfo}
- {showDeleteDialog}
- role="user"
- />
- </div>
- {/if}
- {/if}
- </div>
|