Skip to content
KONIGI

AI Assistants / Turn and response / Response actions

4 of 6

Response actions

Copying, rating, sharing and rerunning all attach to one message, and none of them can dominate it.

Updated September 12, 2026

Problem

An answer arrives and half a dozen things can be done to it. Copy it, rate it, run it again, read it aloud, share it, report it. All six attach to the same block of text, none of them is the reason the viewer is here, and a row of six buttons under every turn turns a readable transcript into a control panel.

Solution

Put the actions in a single row under the turn, left-aligned to the text, and rank them by what people actually do rather than by what the team wants to learn.

Copy dominates. The overwhelming majority of interaction with this row is someone taking the answer somewhere else, and copy deserves the first position and an unambiguous icon. Rating is the inverse: it’s the control teams most want and viewers least use, and putting thumbs first because the model team needs training signal is the standard failure of this pattern.

Visibility is the second decision. Always-on rows create six repeated icons down the page, which is visual noise proportional to conversation length. Hover-reveal keeps the page calm and breaks on touch, where there’s no hover, and for keyboard users, unless focus reveals the row the same way the pointer does. The usable compromise is to pin the row on the most recent turn, where it’s nearly always used, and reveal it on the rest.

Feedback deserves more care than it gets. A bare thumbs-down collects one bit and can’t distinguish wrong from rude from irrelevant from too long. HAX guideline 15 asks for granular feedback, and the cheap version is a single optional follow-up after the click, offering three or four categories and a free-text box nobody has to fill. PAIR’s point about feedback is the other half: tell people what it’ll be used for. A control that silently sends a signal into a training pipeline is asking for trust it hasn’t explained.

The row grows. Every team ships a feature that wants a button there, and four becomes seven over a year. An overflow menu with the top two or three promoted is the only thing that holds the line.

Use when

Assistant turns that people act on, which is nearly all of them in a general assistant.

Don’t use when

The response is ephemeral or inline, like a ghost-text completion or a one-line inline rewrite. There the accept and reject controls are the action row, and adding copy or rating alongside them confuses the primary decision.

Trade-offs

Hover-reveal trades discoverability for calm, and the people most hurt are the ones least able to complain about it. Pinning the row to the last turn only helps if the last turn is the one people act on, which stops being true the moment a conversation becomes a reference document. Icons alone are compact and ambiguous, particularly the two arrows that could equally mean regenerate or share. A thumbs pair implies the feedback goes somewhere and does something. That obliges the product to close the loop, or to stop implying there is one.

Checklist

  • Is copy the easiest thing in the row to hit?
  • Does the row reach keyboard and touch users, or only a pointer?
  • What’s the order, and is it ranked by viewer behaviour or by internal need?
  • Does a thumbs-down collect anything beyond one bit?
  • Is it clear what happens to a rating after it is clicked?
  • Does copy take the markdown, the rendered text, or the raw response?
  • How many items does the row hold, and what’s the rule for adding the next one?
  • Do the actions appear before the response has finished streaming?
  • Can a single turn be shared or linked without sharing the whole conversation?
  • Are the icons legible without their tooltips?

Compare

ChatGPT runs a compact row under each answer with copy, a thumbs pair, read-aloud and regenerate, and reveals it on the turn rather than holding it always-on down the page. Claude keeps the row narrower and moves the heavier actions onto the generated document when one exists, so the transcript row stays about the text and the artifact carries its own controls. Gemini treats sharing as a first-class action rather than an overflow item, exporting a response into a Doc. The row becomes an exit ramp into another product rather than a clipboard. Perplexity anchors its row on rewriting and re-sourcing instead of rating, which fits a product where the next move is usually another search rather than a judgement about the last answer.

Message turn is the block this row attaches to and must not compete with. Regenerate is the heaviest action in the row and the one with a real cost behind it. Artifact panel takes actions out of the row when the output is a document. Code block actions is the same problem one level down, inside a single fenced block. Streaming response determines when the row is allowed to appear.

Response actions anatomy The control row under an assistant turn: copy first because it is used most, then a thumbs pair, then regenerate, with the remainder behind an overflow menu. A follow-up question after a thumbs-down collects a category instead of one bit. Ranked by what people do, not by what the team wants copy + retry What went wrong? wrong too long off topic used to tune retrieval, not to train the model 1 2 3 4 1 COPY COMES FIRST Most interaction with this row is taking the answer somewhere else. 2 THE OVERFLOW Every team wants a button here. Four becomes seven without a rule. 3 GRANULAR FEEDBACK A bare thumbs-down collects one bit and cannot tell wrong from long from rude. 4 SAY WHERE IT GOES A control that quietly feeds a training pipeline is asking for unexplained trust. A row revealed only on hover reaches neither touch nor keyboard, and those users complain least.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

The control row ranked by what people actually do. Copy comes first because it is used most, then the thumbs pair, then retry, with the rest behind an overflow. A thumbs-down that collects a category is worth more than one that collects a bit.

shadcn
npx shadcn@latest add button dropdown-menu
npm
lucide-react
Tokens
--card--card-foreground--muted--muted-foreground--border--chart-1

The break clause is at 12.1: six months' written notice, exercisable at the end of year two, conditional on the rent being paid up to date and vacant possession on the break date.

What went wrong?

used to tune retrieval, not to train the model

ResponseActions.tsxFour in the row and the rest in an overflow. A thumbs-down asks for a reason from a closed set, and the text saying where a rating goes is a required prop.

import { Copy, RotateCcw, ThumbsDown, ThumbsUp, MoreHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

export type Rating = "up" | "down" | null;

/** A thumbs-down is one bit. The follow-up turns it into a category, and the
 *  set is closed so the numbers add up across turns. */
export type Reason = "wrong" | "too long" | "off topic";
export const REASONS: Reason[] = ["wrong", "too long", "off topic"];

export type OverflowItem = { label: string; onSelect: () => void };

type Props = {
  /** What copy takes. The caller decides markdown, rendered text or raw. */
  copyText: string;
  rating: Rating;
  onRate: (rating: Rating) => void;
  onReason: (reason: Reason) => void;
  onRetry: () => void;
  /** Everything past the top four. Read aloud, share, report: they exist,
   *  they are one click further away. */
  overflow: OverflowItem[];
  /** Where a rating goes once clicked. Required, because a control that
   *  quietly feeds a pipeline is asking for trust it has not explained. */
  feedbackUse: string;
  /** The newest turn keeps its row; older turns reveal it on hover or focus,
   *  so the keyboard reaches it the same way the pointer does. */
  pinned?: boolean;
  onCopy?: () => void;
};

export function ResponseActions({
  copyText, rating, onRate, onReason, onRetry, overflow, feedbackUse, pinned = false, onCopy,
}: Props) {
  const copy = () => {
    navigator.clipboard?.writeText(copyText);
    onCopy?.();
  };
  const reveal = pinned ? "" : "opacity-0 group-hover:opacity-100 group-focus-within:opacity-100";

  return (
    <div className={`text-[11px] ${reveal}`}>
      <div className="flex items-center gap-2">
        <Button variant="outline" size="sm" className="h-7 gap-1.5 border-chart-1 px-2.5 text-[11px] text-chart-1 hover:text-chart-1" onClick={copy}>
          <Copy className="size-3" /> copy
        </Button>
        <Button variant="outline" size="sm" className={`h-7 w-8 px-0 ${rating === "up" ? "text-card-foreground" : "text-muted-foreground"}`} aria-label="helpful" aria-pressed={rating === "up"} onClick={() => onRate(rating === "up" ? null : "up")}>
          <ThumbsUp className="size-3" />
        </Button>
        <Button variant="outline" size="sm" className={`h-7 w-8 px-0 ${rating === "down" ? "text-card-foreground" : "text-muted-foreground"}`} aria-label="not helpful" aria-pressed={rating === "down"} onClick={() => onRate(rating === "down" ? null : "down")}>
          <ThumbsDown className="size-3" />
        </Button>
        <Button variant="outline" size="sm" className="h-7 gap-1.5 px-2.5 text-[11px] text-card-foreground" onClick={onRetry}>
          <RotateCcw className="size-3" /> retry
        </Button>
        <DropdownMenu modal={false}>
          <DropdownMenuTrigger asChild>
            <Button variant="outline" size="sm" className="h-7 w-8 px-0 text-muted-foreground" aria-label="more">
              <MoreHorizontal className="size-3" />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="start">
            {overflow.map((item) => (
              <DropdownMenuItem key={item.label} onSelect={item.onSelect}>{item.label}</DropdownMenuItem>
            ))}
          </DropdownMenuContent>
        </DropdownMenu>
      </div>

      {/* Optional, one click, and it says where the answer goes. */}
      {rating === "down" && (
        <div className="ml-24 mt-4 w-[340px] max-w-full rounded-lg border bg-muted p-3">
          <p className="text-sm text-card-foreground">What went wrong?</p>
          <div className="mt-2 flex flex-wrap gap-2">
            {REASONS.map((r) => (
              <Button key={r} variant="outline" size="sm" className="h-6 rounded-full px-2.5 text-[11px]" onClick={() => onReason(r)}>{r}</Button>
            ))}
          </div>
          <p className="mt-3 text-muted-foreground">{feedbackUse}</p>
        </div>
      )}
    </div>
  );
}

demo.tsxHow it is called: the newest turn, pinned, opened on a thumbs-down with the follow-up showing.

import { useState } from "react";
import { ResponseActions, type Rating } from "./ResponseActions";

const ANSWER =
  "The break clause is at 12.1: six months' written notice, exercisable at the end of year two, conditional on the rent being paid up to date and vacant possession on the break date.";

/**
 * The newest turn, so the row is pinned. It opens on a thumbs-down with the
 * follow-up showing; picking a reason closes it, and thumbs-up clears it.
 */
export default function Demo() {
  const [rating, setRating] = useState<Rating>("down");
  return (
    <div className="group rounded-lg border bg-card p-4">
      <p className="text-sm leading-relaxed text-card-foreground">{ANSWER}</p>
      <div className="mt-4">
        <ResponseActions
          copyText={ANSWER}
          rating={rating}
          onRate={setRating}
          onReason={() => setRating(null)}
          onRetry={() => {}}
          overflow={[
            { label: "read aloud", onSelect: () => {} },
            { label: "share this turn", onSelect: () => {} },
            { label: "report", onSelect: () => {} },
          ]}
          feedbackUse="used to tune retrieval, not to train the model"
          pinned
        />
      </div>
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No captures reference this pattern yet. Captures arrive product by product; see Products for what's in the gallery so far.