UI kit

AnswerActions

ChatReact ยท Next.js

A compact row of actions under a generated answer: copy to clipboard, regenerate, and an optional quick ๐Ÿ‘/๐Ÿ‘Ž rating. Wire onRegenerate to re-run the query and onRate to your feedback store.

<AnswerActions />

Live preview

Hybrid retrieval combines dense vector search with BM25, then reranks before generation.

Installation

No dependencies to install โ€” just React + Tailwind CSS. It's a presentational component (no data fetching), so the same source works unchanged in React and Next.js (App or Pages Router) โ€” there's no separate variant. Copy it into your project (e.g. components/AnswerActions.tsx) and import it.

Usage

usage.tsx
import { AnswerActions } from "@/components/AnswerActions";

export default function Example() {
  return (
    <AnswerActions answer={result.answer} onRegenerate={rerun} />
  );
}

Props

PropTypeDefaultDescription
answerrequiredstringโ€”The answer text (used by the copy action).
onRegenerate() => voidโ€”Called to re-run the query.
onRate(value: "up" | "down") => voidโ€”Quick rating callback.

Component source copy-paste ready

Presentational component โ€” no data fetching, so the same source works unchanged in React and Next.js. There's no separate Next.js variant to copy.

AnswerActions.tsx
"use client";

import { useState, type ReactNode } from "react";

interface AnswerActionsProps {
  /** The answer text โ€” used by the copy action. */
  answer: string;
  onRegenerate?: () => void;
  /** Quick rating callback (thumbs up/down). */
  onRate?: (value: "up" | "down") => void;
  className?: string;
}

/**
 * AnswerActions โ€” a row of actions under a generated answer: copy, regenerate,
 * and (optionally) a quick ๐Ÿ‘/๐Ÿ‘Ž rating. Wire onRegenerate to re-run the query
 * and onRate to your feedback store.
 */
export function AnswerActions({ answer, onRegenerate, onRate, className = "" }: AnswerActionsProps) {
  const [copied, setCopied] = useState(false);

  async function copy() {
    try {
      await navigator.clipboard.writeText(answer);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch {}
  }

  return (
    <div className={`flex items-center gap-1 ${className}`}>
      <Action label={copied ? "Copied" : "Copy"} onClick={copy}>
        {copied ? (
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M20 6 9 17l-5-5" /></svg>
        ) : (
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg>
        )}
      </Action>

      {onRegenerate && (
        <Action label="Regenerate" onClick={onRegenerate}>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 12a9 9 0 1 0 3-6.7L3 8" /><path d="M3 3v5h5" /></svg>
        </Action>
      )}

      {onRate && (
        <>
          <Action label="Good answer" onClick={() => onRate("up")}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M7 10v12" /><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z" /></svg>
          </Action>
          <Action label="Bad answer" onClick={() => onRate("down")}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ transform: "rotate(180deg)" }}><path d="M7 10v12" /><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z" /></svg>
          </Action>
        </>
      )}
    </div>
  );
}

function Action({ label, onClick, children }: { label: string; onClick: () => void; children: ReactNode }) {
  return (
    <button
      type="button"
      onClick={onClick}
      title={label}
      aria-label={label}
      className="flex h-8 w-8 items-center justify-center rounded-md text-zinc-500 transition hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
    >
      {children}
    </button>
  );
}