08 / 37 · Messages
Message actions
Copy, rate, and regenerate. Each action confirms itself with a small state change.
Installation
npx shadcn@latest add "@assistant-ui/elements-message-actions"
Usage
import { MessageActions } from "@/components/elements/message-actions";
<MessageActions
copied={false}
reaction={null}
regenerating={false}
onCopy={() => {}}
onReactionChange={setReaction}
onRegenerate={() => {}}
onMore={() => {}}
/>Props
copied*booleanWhen true the copy button shows a green check confirmation.
reaction*ReactionActive thumbs reaction, or null when none is selected.
regenerating*booleanWhen true the regenerate icon spins to show work in progress.
onCopy*() => voidCalled when the copy control is pressed.
onReactionChange*(reaction: Reaction) => voidCalled when the user toggles thumbs up or thumbs down.
onRegenerate*() => voidCalled when the regenerate control is pressed.
onMore*() => voidCalled when the overflow menu control is pressed.
classNamestringExtra classes merged onto the root.
Source
message-actions.tsx"use client";
import {
CheckIcon,
CopyIcon,
EllipsisIcon,
RefreshCwIcon,
ThumbsDownIcon,
ThumbsUpIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { ghostButton, iconSwap, iconSwapIn, iconSwapOut } from "./surfaces";
export type Reaction = "up" | "down" | null;
export interface MessageActionsProps {
copied: boolean;
reaction: Reaction;
regenerating: boolean;
onCopy: () => void;
onReactionChange: (reaction: Reaction) => void;
onRegenerate: () => void;
onMore: () => void;
className?: string;
}
export function MessageActions({
copied,
reaction,
regenerating,
onCopy,
onReactionChange,
onRegenerate,
onMore,
className,
}: MessageActionsProps) {
const buttonClassName = cn(ghostButton, "size-7");
return (
<div className={cn("flex items-center gap-1", className)}>
<button
type="button"
aria-label={copied ? "Copied response" : "Copy response"}
onClick={onCopy}
className={cn(
buttonClassName,
"grid place-items-center",
copied && "text-emerald-500",
)}
>
<CopyIcon
className={cn(
iconSwap,
"size-3.5",
copied ? iconSwapOut : iconSwapIn,
)}
/>
<CheckIcon
className={cn(
iconSwap,
"size-3.5",
copied ? iconSwapIn : iconSwapOut,
)}
/>
</button>
<button
type="button"
aria-label="Mark response helpful"
aria-pressed={reaction === "up"}
onClick={() => onReactionChange(reaction === "up" ? null : "up")}
className={cn(
buttonClassName,
reaction === "up" &&
"bg-foreground/[0.06] text-foreground/90 dark:bg-foreground/[0.09]",
)}
>
<ThumbsUpIcon className="size-3.5" />
</button>
<button
type="button"
aria-label="Mark response unhelpful"
aria-pressed={reaction === "down"}
onClick={() => onReactionChange(reaction === "down" ? null : "down")}
className={cn(
buttonClassName,
reaction === "down" &&
"bg-foreground/[0.06] text-foreground/90 dark:bg-foreground/[0.09]",
)}
>
<ThumbsDownIcon className="size-3.5" />
</button>
<button
type="button"
aria-label="Regenerate response"
onClick={onRegenerate}
className={buttonClassName}
>
<RefreshCwIcon
className={cn(
"size-3.5",
regenerating && "animate-spin motion-reduce:animate-none",
)}
/>
</button>
<button
type="button"
aria-label="More response actions"
onClick={onMore}
className={buttonClassName}
>
<EllipsisIcon className="size-3.5" />
</button>
</div>
);
}