Skip to content

Custom Actions ​

Add custom action buttons to messages, like your own feedback or "send to CRM" buttons.

New in 0.4.0

Copy, regenerate, edit and thumbs up / down with reasons are now built in: switch them on with the messageActions and feedback props (see Portal Experience). Write your own actions with renderMessageActions only for what the built-in row does not cover.

Built-in or custom ​

You wantUse
Copy, regenerate the newest answer, edit-and-resend your own messagesmessageActions={{ copy: true, regenerate: true, edit: true }}
Thumbs up / down with reasons and a comment, stored by the serverfeedback={{ enabled: true }} (and onFeedback)
Something of your own: save to CRM, create a ticket, sharerenderMessageActions

The built-in row and your own slot can be used together.

Render Message Actions ​

renderMessageActions is drawn under every finished assistant message, hidden until the message is hovered (opacity-0 group-hover:opacity-100). It is not called for the message that is still streaming.

tsx
import { Chat, ThumbsUpIcon, ThumbsDownIcon } from "@cognipeer/chat-ui";

function MessageActions({ message, isStreaming }) {
  // Don't show actions while streaming
  if (isStreaming) return null;

  const handleFeedback = (type: "up" | "down") => {
    // Send to your API
    fetch("/api/feedback", {
      method: "POST",
      body: JSON.stringify({ messageId: message.id, feedback: type }),
    });
  };

  return (
    <div className="flex gap-2">
      <button
        type="button"
        onClick={() => handleFeedback("up")}
        className="p-1 hover:bg-gray-700 rounded"
        title="Good response"
        aria-label="Good response"
      >
        <ThumbsUpIcon className="w-4 h-4" />
      </button>
      <button
        type="button"
        onClick={() => handleFeedback("down")}
        className="p-1 hover:bg-gray-700 rounded"
        title="Bad response"
        aria-label="Bad response"
      >
        <ThumbsDownIcon className="w-4 h-4" />
      </button>
    </div>
  );
}

<Chat
  baseUrl="/api/agents"
  agentId="assistant"
  renderMessageActions={(props) => <MessageActions {...props} />}
/>

The function receives:

typescript
interface MessageActionProps {
  message: Message;
  isStreaming: boolean;
}

Buttons need accessible names (aria-label) because the slot only holds icons; keep them keyboard-reachable (real button elements).

Available Icons ​

Every icon the chat uses is exported, so your buttons match the built-in ones:

tsx
import {
  ThumbsUpIcon, ThumbsDownIcon, CopyIcon, RefreshIcon, PencilIcon, TrashIcon,
  CheckIcon, XIcon, SendIcon, StopIcon, PaperclipIcon, FileIcon, ImageIcon,
  ChatIcon, PlusIcon, MenuIcon, ToolIcon, AlertIcon, InfoIcon, LoadingIcon,
  ChevronDownIcon, ChevronRightIcon, PinIcon, DownloadIcon, BellIcon, ListCheckIcon,
} from "@cognipeer/chat-ui";

Pre-built actions ​

MessageActions is a ready-made row (copy, regenerate, thumbs) you can drop into renderMessageActions:

tsx
import { Chat, MessageActions } from "@cognipeer/chat-ui";

<Chat
  baseUrl="/api/agents"
  renderMessageActions={(props) => (
    <MessageActions
      {...props}
      showRegenerate={false}
      onFeedback={(messageId, type) => save(messageId, type)}   // type: "positive" | "negative"
      onCopy={(content) => analytics.track("copy")}
    />
  )}
/>

It shows nothing for user messages and while streaming. Its feedback state is local to the row (it does not talk to the server); for server-stored ratings with reasons use the built-in feedback prop.

Copy Button ​

tsx
import { copyText } from "@cognipeer/chat-ui";

function CopyAction({ message }) {
  const text = typeof message.content === "string"
    ? message.content
    : message.content.map((p) => (p.type === "text" ? p.text : "")).join("");

  return (
    <button type="button" aria-label="Copy" onClick={() => copyText(text)}>
      <CopyIcon className="w-4 h-4" />
    </button>
  );
}

copyText uses the clipboard API and falls back to the older copy command inside an iframe that has no allow="clipboard-write"; it resolves false when neither worked.

Regenerate Button ​

Regenerating the newest answer is built in (messageActions.regenerate). In a custom layout built on useChat:

tsx
function RegenerateAction() {
  const { regenerate, canRegenerate, isStreaming } = chat;   // from useChat(...)
  if (!canRegenerate) return null;
  return (
    <button type="button" disabled={isStreaming} onClick={() => regenerate()}>
      <RefreshIcon className="w-4 h-4" />
    </button>
  );
}

retry() sends the last message again after an error; regenerate() runs the newest answer again from the message that asked for it (POST /conversations/:id/messages/:messageId/regenerate), and resolves false when it cannot (a turn runs, a card waits, the last message is not an answer).

Feedback with State ​

Track the rating yourself, or let the library do it: with the built-in feedback prop a stored rating comes back on message.feedback (also read from metadata.feedback) and the thumb shows pressed.

tsx
import { useState } from "react";

function FeedbackActions({ message }) {
  const [feedback, setFeedback] = useState<"up" | "down" | null>(null);

  const handleFeedback = async (type: "up" | "down") => {
    const next = feedback === type ? null : type;   // pressing the same thumb again withdraws it
    setFeedback(next);
    await submitFeedback(message.id, next);
  };

  return (
    <div className="flex gap-1">
      <button type="button" aria-pressed={feedback === "up"} onClick={() => handleFeedback("up")}>
        <ThumbsUpIcon className="w-4 h-4" />
      </button>
      <button type="button" aria-pressed={feedback === "down"} onClick={() => handleFeedback("down")}>
        <ThumbsDownIcon className="w-4 h-4" />
      </button>
    </div>
  );
}

For the server-backed version use useChat().submitFeedback(messageId, { rating, reasons, comment }); it shows the rating at once and takes it back if the server refuses.

Styling Actions ​

The built-in row uses the .chat-message-actions class: invisible until the message is hovered or holds keyboard focus, always visible under the newest message and on touch screens. Your own slot is wrapped in a hover fade; style your buttons with the theme variables:

css
.my-action {
  padding: 4px;
  border-radius: 4px;
  color: var(--chat-text-tertiary);
}

.my-action:hover {
  background: var(--chat-bg-hover);
  color: var(--chat-text-primary);
}

Next Steps ​

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub