Elements

15 / 122 · Messages

Edit a sent message

Rewrite a turn in place, told up front how many replies the edit throws away.

sending discards 3 replies

Installation

npx shadcn@latest add "@assistant-ui/elements-edit-message"

Usage

import { EditMessage } from "@/components/elements/edit-message";

<EditMessage
  value={value}
  discardedReplies={3}
  editing={editing}
  onValueChange={setValue}
  onStartEdit={() => setEditing(true)}
  onCancel={cancel}
  onSave={save}
/>

Props

value*stringThe message text, read when resting and edited when open.
discardedReplies*numberHow many replies sending would drop. The warning hides at zero.
editing*booleanSwaps between the resting bubble and the edit composer.
onValueChange(value: string) => voidCalled as the message is edited.
onStartEdit() => voidCalled when the resting bubble is clicked.
onCancel() => voidCalled when the edit is abandoned. Restore the original yourself.
onSave() => voidCalled when the edited message is sent.
classNamestringExtra classes merged onto the root.

Source

edit-message.tsx
"use client";

import { AlertTriangleIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { field, inkButton, mono, paper } from "./surfaces";

export function EditMessage({
  value,
  discardedReplies,
  editing,
  onValueChange,
  onSave,
  onCancel,
  onStartEdit,
  className,
}: {
  value: string;
  discardedReplies: number;
  editing: boolean;
  onValueChange?: (value: string) => void;
  onSave?: () => void;
  onCancel?: () => void;
  onStartEdit?: () => void;
  className?: string;
}) {
  if (!editing) {
    return (
      <div className={cn("flex w-full max-w-sm justify-end", className)}>
        <button
          type="button"
          onClick={onStartEdit}
          className={cn(
            field,
            "hover:bg-foreground/[0.07] max-w-[85%] rounded-2xl px-3.5 py-2.5 text-start text-[13.5px] transition-colors",
          )}
        >
          {value}
        </button>
      </div>
    );
  }

  return (
    <div
      className={cn(
        paper,
        "flex w-full max-w-sm flex-col gap-3 rounded-[20px] p-3.5",
        className,
      )}
    >
      <textarea
        value={value}
        onChange={(event) => onValueChange?.(event.target.value)}
        rows={2}
        aria-label="Edit your message"
        className={cn(
          field,
          "text-foreground/90 focus-visible:ring-foreground/20 resize-none rounded-xl px-3 py-2.5 text-[13.5px] leading-relaxed outline-none focus-visible:ring-2",
        )}
      />

      {discardedReplies > 0 && (
        <div className="flex items-center gap-2 text-amber-700 dark:text-amber-400">
          <AlertTriangleIcon className="size-3.5 shrink-0" />
          <span className={cn(mono, "tabular-nums")}>
            sending discards {discardedReplies}{" "}
            {discardedReplies === 1 ? "reply" : "replies"}
          </span>
        </div>
      )}

      <div className="flex items-center justify-end gap-2">
        <button
          type="button"
          onClick={onCancel}
          className="text-foreground/55 hover:bg-foreground/[0.06] hover:text-foreground/90 h-8 rounded-full px-3.5 text-xs font-medium transition-[background-color,color,scale] duration-150 active:scale-[0.96]"
        >
          Cancel
        </button>
        <button
          type="button"
          onClick={onSave}
          className={cn(
            inkButton,
            "flex h-8 items-center rounded-full px-3.5 text-xs font-medium",
          )}
        >
          Send
        </button>
      </div>
    </div>
  );
}