Response feedback
ProA feedback control for AI replies where thumbs down opens an inline panel with reason chips and an optional note, thumbs up fills and asks what was good, and a submit shows a short thank-you before folding back into the row.
Why did our orders query get slower after we added an index on created_at?
Most likely the planner now prefers the new index for ORDER BY created_at, then filters status = 'paid' row by row. With 4.2M orders and only 3% paid, that walks most of the table.
- Run
EXPLAIN (ANALYZE, BUFFERS)and look for an Index Scan onorders_created_at_idxwith a large Rows Removed by Filter. - Replace it with a composite index on
(status, created_at), so the filter and the sort use one index. - Run
ANALYZE ordersafterwards, so the planner sees fresh statistics.
- Collecting thumbs up and down on AI replies in a chat, copilot or search answer.
- Gathering structured reasons (inaccurate, unsafe) for model evaluation or support triage.
- Adding a message action row with copy and regenerate next to feedback.
- Star or numeric ratings of a product or session: use rating.
- General product feedback not tied to one reply: use feedback-morph.
- Long surveys with many questions: use a form or dialog.
Installation
Pro source and install commands unlock with a Pro plan.
import { FeedbackActions, ResponseFeedback } from "@/components/arc/response-feedback/response-feedback"; // The full reply card<ResponseFeedback model={{ name: "Claude Opus 5.5" }} prompt={question} copyText={answerText} messageId={message.id} onFeedback={async (event, { signal }) => { await fetch("/api/feedback", { method: "POST", body: JSON.stringify(event), signal }); }}> <Markdown>{answerText}</Markdown></ResponseFeedback> // Just the control, inside your own message row<FeedbackActions messageId={message.id} defaultRating={message.rating} leading={<CopyButton value={answerText} iconOnly variant="plain" label="Copy reply" />} onFeedback={saveFeedback}/>API reference
2 parts. The first is the root.
ResponseFeedback
An AI reply card: optional prompt bubble, model mark and name, the reply body and an action row with copy, optional regenerate and the thumbs feedback control. With no props it renders a sample Claude Sonnet 5.5 answer with simulated feedback. Accepts every FeedbackActions prop except leading.
model{ name: string; mark?: ReactNode }{ name: "Claude Sonnet 5.5" }The model that wrote the reply. The mark is drawn at 16px and defaults to the Claude mark.promptReactNode | null–The person's message shown above the reply. Defaults to a sample question; pass null to hide it.childrenReactNode–The reply body. Paragraphs, lists and inline code are styled. Defaults to a sample Postgres answer.copyTextstring | null–Plain text for the copy action. Defaults to the sample reply when no children are given; pass null to hide copy.onRegenerate() => void–Shows a regenerate action in the row.onFeedback(event: FeedbackEvent, context: { signal: AbortSignal }) => void | Promise<void>–Persists feedback. Fires with stage "rating" on every thumb press and stage "details" when reasons or a note are submitted. Reject to show the inline error and Try again; the signal aborts when the panel closes or the rating changes. Without it, sends are simulated.messageIdstring–Passed back on every event so feedback can be stored against a message.rating"up" | "down" | null–Controlled rating. Leave undefined to let the control own it.defaultRating"up" | "down" | nullnullInitial rating when uncontrolled, for example a rating loaded from your database.onRatingChange(rating: "up" | "down" | null) => void–Called on every rating change, including clearing it by pressing the active thumb again.negativeReasonsstring[]["Inaccurate", "Not helpful", "Too long", "Unsafe", "Other"]Reason chips offered after a thumbs down. A chip labelled Other moves focus to the note and changes its placeholder.positiveReasonsstring[]["Accurate", "Easy to follow", "Good examples", "Saved me time"]Chips offered after a thumbs up. Pass [] to skip the follow-up row.allowCommentbooleantrueShows the optional note field after a thumbs down.commentPlaceholderstring"Add details (optional)"Placeholder for the note field.commentMaxLengthnumber1000Maximum note length.privacyNoteReactNode"Shares this reply for review"Quiet line next to Submit about what gets shared. Hidden below 400px of container width.thanksMessageReactNode–Second line of the thank-you state. Defaults to a simulated note without onFeedback, otherwise "It helps improve future replies."thanksDurationnumber1600How long the thank-you state holds before the panel folds away, in ms.trailingReactNode–Content pinned to the end of the row, such as a timestamp or token count.disabledbooleanfalseDisables the thumbs, for example while the reply is still streaming.classNamestring–Class on the root element.FeedbackActions
The feedback control on its own, for your own message rows: thumbs up and down, the reasons panel, the What was good row and the thank-you state.
leadingReactNode–Actions rendered before the thumbs, such as copy or regenerate. Use 16px icons in control-height-sm buttons to match.onFeedback(event: FeedbackEvent, context: { signal: AbortSignal }) => void | Promise<void>–Persists feedback. Fires with stage "rating" on every thumb press and stage "details" when reasons or a note are submitted. Reject to show the inline error and Try again; the signal aborts when the panel closes or the rating changes. Without it, sends are simulated.messageIdstring–Passed back on every event so feedback can be stored against a message.rating"up" | "down" | null–Controlled rating. Leave undefined to let the control own it.defaultRating"up" | "down" | nullnullInitial rating when uncontrolled, for example a rating loaded from your database.onRatingChange(rating: "up" | "down" | null) => void–Called on every rating change, including clearing it by pressing the active thumb again.negativeReasonsstring[]["Inaccurate", "Not helpful", "Too long", "Unsafe", "Other"]Reason chips offered after a thumbs down. A chip labelled Other moves focus to the note and changes its placeholder.positiveReasonsstring[]["Accurate", "Easy to follow", "Good examples", "Saved me time"]Chips offered after a thumbs up. Pass [] to skip the follow-up row.allowCommentbooleantrueShows the optional note field after a thumbs down.commentPlaceholderstring"Add details (optional)"Placeholder for the note field.commentMaxLengthnumber1000Maximum note length.privacyNoteReactNode"Shares this reply for review"Quiet line next to Submit about what gets shared. Hidden below 400px of container width.thanksMessageReactNode–Second line of the thank-you state. Defaults to a simulated note without onFeedback, otherwise "It helps improve future replies."thanksDurationnumber1600How long the thank-you state holds before the panel folds away, in ms.trailingReactNode–Content pinned to the end of the row, such as a timestamp or token count.disabledbooleanfalseDisables the thumbs, for example while the reply is still streaming.classNamestring–Class on the root element.- Tab
- Moves through copy, thumbs up and thumbs down, then into the open panel.
- EnterorSpace
- Presses a thumb, toggles a reason chip, or activates Submit, Send, Skip and Cancel. Pressing the active thumb clears the rating.
- Escape
- Closes the open panel and returns focus to the thumb that opened it. The rating stays.
- CmdorCtrl+Enter
- Submits from the note field.
- Thumbs are toggle buttons with aria-pressed and labels Good response and Bad response; aria-expanded and aria-controls point at the panel while it is open.
- Reason chips are toggle buttons with aria-pressed inside a labelled group. Selected chips invert to a solid fill, so the state never relies on hue.
- A keyboard press on thumbs down moves focus to the first reason; a pointer press leaves focus where it is. After a keyboard submit, focus lands on the thank-you line and returns to the thumb when the panel folds.
- A polite live region announces rating changes, sending, the thank-you state, errors, and says when nothing was sent because the send is simulated.
- Errors show an icon and text next to the actions, and Submit becomes Try again.
- The panel opens and closes on a bounce-free spring (0.24s visual duration) that animates measured height, with a 160ms opacity fade.
- Switching between the form and the thank-you line is a single crossfade (out 100ms, in 160ms, 2px rise) while the panel height springs to the new content.
- The filled thumb fades in over 160ms on top of the outline, so nothing moves or resizes. Presses scale to .94 instantly and release on a spring.
- The success tick draws once in 280ms.
- Reduced motion: height changes cut instantly, the panel only fades, and press scales are dropped. All end states and announcements stay.
- The card pads 16px below 480px of container width and 24px above; the panel and chips wrap, so it holds at 360px without horizontal scroll.
- Below 400px of container width the privacy note and Cancel give way (Close covers Cancel), so the error line and Try again always fit on one row.
- Chips grow to 32px tall on coarse pointers.
- Two small motion trees (panel height and content crossfade) mount only while the panel is open.
- One ResizeObserver per open panel animates content height changes; it disconnects when the panel closes.
- No timers run while idle; the thank-you timer and in-flight sends are cleared on unmount.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Use ResponseFeedback for a single assistant reply with its actions; use FeedbackActions when you already render your own messages.
- Persist both stages: store the rating on stage "rating" (null means cleared) and attach reasons and the note on stage "details". Always pass messageId.
- Throw or reject from onFeedback to show the error state; the component handles Try again and aborts stale calls through the signal.
- Set disabled while the reply is still streaming, so people rate the finished answer.
- Keep reason labels short (one or two words) so the chips stay on one or two lines at 360px.
The full library index for assistants is at /llms.txt.