AnswerActions
ChatReact ยท Next.jsA 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
import { AnswerActions } from "@/components/AnswerActions";
export default function Example() {
return (
<AnswerActions answer={result.answer} onRegenerate={rerun} />
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
answerrequired | string | โ | 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.
"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>
);
}