EditMessageComposer
Edit a sent user message in place. Enter resends the trimmed text, Shift+Enter adds a line and Escape cancels; the composer stays open with a loader until onSubmit settles. MessageList renders it automatically when messageActions.onEdit is set.
Getting Started
$pnpm add @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-reactExamples
Inline edit
Source
The component source as it ships in the package. Useful as a reference, or as a starting point if you want to fork a piece.
message-actions/EditMessageComposer/EditMessageComposer.tsx
import React, { memo, useState } from 'react';
import { Alert, Button, Group, Paper, Stack, Text, Textarea } from '@mantine/core';
import { IconAlertCircle } from '@tabler/icons-react';
import { useAsyncAction } from '../use-async-action';
export interface EditMessageComposerLabels {
input: string;
placeholder: string;
save: string;
cancel: string;
hint: string;
error: string;
}
export const DEFAULT_EDIT_MESSAGE_COMPOSER_LABELS: EditMessageComposerLabels = {
input: 'Edit message',
placeholder: 'Edit your message',
save: 'Save & resend',
cancel: 'Cancel',
hint: 'Enter to send, Shift+Enter for a new line, Esc to cancel',
error: 'Could not resend the message',
};
export interface EditMessageComposerProps {
/** Text the editor starts with, usually the original message */
defaultValue: string;
/** Saves and resends the edited text, trimmed; the composer stays open and shows a loader until the promise settles */
onSubmit: (text: string) => void | Promise<void>;
/** Leaves editing without saving */
onCancel: () => void;
/** Maximum number of rows before the editor scrolls, `10` by default */
maxRows?: number;
/** Shows the keyboard hint, `true` by default */
withHint?: boolean;
/** Overrides of the default English labels */
labels?: Partial<EditMessageComposerLabels>;
/** Class name added to the root element */
className?: string;
/** Inline styles added to the root element */
style?: React.CSSProperties;
}
/** Inline editor that replaces a user message: Enter resends, Escape cancels */
export const EditMessageComposer = memo(function EditMessageComposer({
defaultValue,
onSubmit,
onCancel,
maxRows = 10,
withHint = true,
labels: labelsProp,
className,
style,
}: EditMessageComposerProps) {
const labels = { ...DEFAULT_EDIT_MESSAGE_COMPOSER_LABELS, ...labelsProp };
const [value, setValue] = useState(defaultValue);
const { pendingKey, error, run } = useAsyncAction(labels.error);
const isPending = pendingKey !== null;
const trimmed = value.trim();
const submit = () => {
if (!trimmed || isPending) {
return;
}
run('save', () => onSubmit(trimmed));
};
return (
<Paper withBorder radius="md" p="xs" className={className} style={style}>
<Stack gap="xs">
<Textarea
aria-label={labels.input}
placeholder={labels.placeholder}
variant="unstyled"
autosize
minRows={1}
maxRows={maxRows}
autoFocus
value={value}
readOnly={isPending}
onFocus={(event) => {
const length = event.currentTarget.value.length;
event.currentTarget.setSelectionRange(length, length);
}}
onChange={(event) => setValue(event.currentTarget.value)}
onKeyDown={(event) => {
if (event.key === 'Escape') {
event.preventDefault();
if (!isPending) {
onCancel();
}
return;
}
if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing) {
event.preventDefault();
submit();
}
}}
/>
{error && (
<Alert color="red" variant="light" p="xs" icon={<IconAlertCircle size={16} />}>
{error}
</Alert>
)}
<Group gap="xs" justify="space-between" wrap="nowrap">
{withHint ? (
<Text size="xs" c="dimmed" truncate="end" miw={0}>
{labels.hint}
</Text>
) : (
<span />
)}
<Group gap="xs" wrap="nowrap">
<Button size="xs" variant="subtle" color="gray" onClick={onCancel} disabled={isPending}>
{labels.cancel}
</Button>
<Button size="xs" onClick={submit} disabled={!trimmed} loading={isPending}>
{labels.save}
</Button>
</Group>
</Group>
</Stack>
</Paper>
);
});
EditMessageComposer.displayName = 'EditMessageComposer';
API reference
Prop
Type
Required
defaultValue
string
Yes
Text the editor starts with, usually the original message
onSubmit
(text: string) => void | Promise<void>
Yes
Saves and resends the edited text, trimmed; the composer stays open and shows a loader until the promise settles
onCancel
() => void
Yes
Leaves editing without saving
maxRows
number
No
Maximum number of rows before the editor scrolls, `10` by default
withHint
boolean
No
Shows the keyboard hint, `true` by default
labels
Partial<EditMessageComposerLabels>
No
Overrides of the default English labels
className
string
No
Class name added to the root element
style
React.CSSProperties
No
Inline styles added to the root element