import { fileKindLabel } from "../../i18n/labels"; import { useState, type FormEvent, type ReactNode } from "react"; import { Avatar, Button, Icon, IconButton } from "../../shared/ui"; import type { ChatAttachmentRecord, ChatMentionRecord, ChatMessageRecord, ChatSlashInvocationRecord, FileObjectRecord, } from "../../../../shared/domain"; import { formatTime } from "../types"; import { messageAttachments, senderName, type MessageContext } from "./chat-helpers"; import type { ThreadLink } from "./thread-links"; import { MessageMarkdown } from "./MessageMarkdown"; import type { ChatReplyStatus } from "../../../../shared/chat-domain"; import { t } from "../../i18n/i18n"; export function ChatMessageRow({ message, attachments, mentions, slashInvocations, files, context, canModify = false, thread, replyStatuses = [], onOpenThread, onEdit, onDelete, onOpenFile, }: { message: ChatMessageRecord; attachments: ChatAttachmentRecord[]; mentions: ChatMentionRecord[]; slashInvocations: ChatSlashInvocationRecord[]; files: FileObjectRecord[]; context: MessageContext; canModify?: boolean; /** Where the message sits in its thread; absent inside the thread view itself. */ thread?: ThreadLink; /** Where each agent's reply to this message stands; a finished reply shows nothing. */ replyStatuses?: ChatReplyStatus[]; onOpenThread?: () => void; /** Saves an edit; the editor stays open until it is saved. */ onEdit?: (bodyMd: string) => Promise; onDelete?: () => void; onOpenFile?: (fileId: string) => void; }) { const [editing, setEditing] = useState(false); const [saving, setSaving] = useState(false); const [editError, setEditError] = useState(null); const name = senderName(message, context); const bot = message.senderType === "agent"; const deleted = Boolean(message.deletedAt); const messageFiles = deleted ? [] : messageAttachments(message.id, attachments, files); const messageMentions = mentions.filter((mention) => mention.messageId === message.id); const slashInvocation = deleted ? null : slashInvocations.find((invocation) => invocation.messageId === message.id) ?? null; const readers = bot || deleted ? [] : context.readersByMessage.get(message.id) ?? []; const submitEdit = (event: FormEvent) => { event.preventDefault(); const next = new FormData(event.currentTarget).get("body"); const value = typeof next === "string" ? next.trim() : ""; if (!value || !onEdit || saving) return; setSaving(true); setEditError(null); // The editor closes only once the edit is saved; a refusal keeps the text and says why. onEdit(value).then(() => { setSaving(false); setEditing(false); }).catch((reason: unknown) => { const message = reason instanceof Error ? reason.message : String(reason); console.error(`[chat] edit failed: ${message}`); setSaving(false); setEditError(message); }); }; return (
{name} {formatTime(message.createdAt)} {message.editedAt && !deleted && {t("(edited)")}} {!editing && (canModify || (onOpenThread && !deleted)) && ( {onOpenThread && !deleted && } {canModify && setEditing(true)} />} {canModify && } )}
{thread?.replyTo && ( )} {slashInvocation && (
/{slashInvocation.command}
)} {deleted ? (
{t("Message deleted")}
) : editing ? (