17 / 122 · Messages
Feedback dialog
A thumbs-down that asks why, so the signal arrives with a reason attached.
What went wrong?optional
Installation
npx shadcn@latest add "@assistant-ui/elements-feedback-dialog"
Usage
import { FeedbackDialog } from "@/components/elements/feedback-dialog";
<FeedbackDialog
reasons={["Not factual", "Too long"]}
selected={selected}
note={note}
sent={sent}
onToggleReason={toggle}
onNoteChange={setNote}
onSubmit={send}
/>Props
reasons*string[]The reason chips offered. Keep them short enough to scan in one line.
selected*string[]Which reasons are currently picked. More than one is allowed.
note*stringFree-text detail, always optional.
sent*booleanReplaces the whole form with an acknowledgement once the report is filed.
onToggleReason(reason: string) => voidCalled when a reason chip is picked or unpicked.
onNoteChange(note: string) => voidCalled as the free-text note is typed.
onSubmit() => voidCalled when the report is sent.
classNamestringExtra classes merged onto the root.
Source
feedback-dialog.tsx"use client";
import { CheckIcon, ThumbsDownIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { field, inkButton, mono, paper } from "./surfaces";
export function FeedbackDialog({
reasons,
selected,
note,
sent,
onToggleReason,
onNoteChange,
onSubmit,
className,
}: {
reasons: readonly string[];
selected: readonly string[];
note: string;
sent: boolean;
onToggleReason?: (reason: string) => void;
onNoteChange?: (note: string) => void;
onSubmit?: () => void;
className?: string;
}) {
if (sent) {
return (
<div
className={cn(
paper,
"fade-in animate-in flex w-full max-w-sm items-center gap-2.5 rounded-[20px] p-4 text-[13.5px] duration-300",
className,
)}
>
<CheckIcon className="size-4 shrink-0 text-emerald-500" />
Thanks. That helps us tune the model.
</div>
);
}
return (
<div
className={cn(
paper,
"flex w-full max-w-sm flex-col gap-3 rounded-[20px] p-4",
className,
)}
>
<div className="flex items-center gap-2.5">
<span className="bg-foreground/[0.05] text-foreground/45 flex size-7 shrink-0 items-center justify-center rounded-lg">
<ThumbsDownIcon className="size-3.5" />
</span>
<span className="text-[13.5px] font-medium">What went wrong?</span>
<span className={cn(mono, "text-foreground/30 ms-auto")}>optional</span>
</div>
<div className="flex flex-wrap gap-1.5">
{reasons.map((reason) => {
const active = selected.includes(reason);
return (
<button
key={reason}
type="button"
aria-pressed={active}
onClick={() => onToggleReason?.(reason)}
className={cn(
"rounded-full px-2.5 py-1 text-xs transition-[background-color,color,scale] duration-150 active:scale-[0.96]",
active
? "bg-foreground text-background"
: cn(field, "text-foreground/55 hover:text-foreground/90"),
)}
>
{reason}
</button>
);
})}
</div>
<textarea
value={note}
onChange={(event) => onNoteChange?.(event.target.value)}
rows={2}
placeholder="Anything else?"
className={cn(
field,
"text-foreground/80 placeholder:text-foreground/30 focus-visible:ring-foreground/20 resize-none rounded-xl px-3 py-2 text-xs outline-none focus-visible:ring-2",
)}
/>
<button
type="button"
onClick={onSubmit}
className={cn(
inkButton,
"flex h-8 items-center justify-center self-end rounded-full px-3.5 text-xs font-medium",
)}
>
Send feedback
</button>
</div>
);
}