UI kit

FeedbackBar

FeedbackReact ยท Next.js

Collects a quick helpfulness rating on an answer, with an optional comment when the user rates it down. Wire it to an endpoint (or onFeedback) to capture eval signal for your pipeline's quality loop.

<FeedbackBar />

Live preview

Was this helpful?

Installation

No npm dependencies โ€” just React + Tailwind CSS. There are two ways to use it:

  • React โ€” copy the source below; it calls your pipeline directly from the browser.
  • Next.js โ€” switch to the Next.js tab in the source below; it keeps your pipeline URL + key server-side behind a Route Handler (adds a couple of .env.local vars).

Usage

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

export default function Example() {
  return (
    <FeedbackBar endpoint="https://api.example.com/feedback" messageId={id} />
  );
}

Props

PropTypeDefaultDescription
endpointstringโ€”POST { messageId, value, comment } to collect feedback.
messageIdstringโ€”Identifier for the rated message.
onFeedback(value, comment?) => voidโ€”Local callback when feedback is submitted.
apiKeystringโ€”Sent as X-API-Key when your endpoint requires it.
fetchertypeof fetchโ€”Override the transport. Defaults to window.fetch.

Component source copy-paste ready

FeedbackBar.tsx
"use client";

import { useState } from "react";

interface FeedbackBarProps {
  /** Optional: POST { messageId, value, comment } to collect feedback. */
  endpoint?: string;
  messageId?: string;
  apiKey?: string;
  /** Called locally when the user submits feedback. */
  onFeedback?: (value: "up" | "down", comment?: string) => void;
  /** Override the transport (e.g. for testing). Defaults to window.fetch. */
  fetcher?: typeof fetch;
  className?: string;
}

/**
 * FeedbackBar โ€” a ๐Ÿ‘/๐Ÿ‘Ž rating on an answer with an optional comment on
 * thumbs-down. Wire it to an endpoint (or onFeedback) to capture eval signal
 * for your pipeline's quality loop.
 */
export function FeedbackBar({ endpoint, messageId, apiKey, onFeedback, fetcher, className = "" }: FeedbackBarProps) {
  const [value, setValue] = useState<"up" | "down" | null>(null);
  const [comment, setComment] = useState("");
  const [sent, setSent] = useState(false);

  async function submit(v: "up" | "down", c?: string) {
    onFeedback?.(v, c);
    if (endpoint) {
      try {
        const f = fetcher ?? fetch;
        await f(endpoint, {
          method: "POST",
          headers: { "Content-Type": "application/json", ...(apiKey ? { "X-API-Key": apiKey } : {}) },
          body: JSON.stringify({ messageId, value: v, comment: c ?? null }),
        });
      } catch {}
    }
    setSent(true);
  }

  if (sent) return <p className={`text-xs text-zinc-400 ${className}`}>Thanks for the feedback.</p>;

  return (
    <div className={`flex flex-col gap-2 ${className}`}>
      <div className="flex items-center gap-2">
        <span className="text-xs text-zinc-500">Was this helpful?</span>
        <button
          type="button"
          aria-label="Helpful"
          onClick={() => { setValue("up"); submit("up"); }}
          className={`flex h-7 w-7 items-center justify-center rounded-md border transition ${
            value === "up"
              ? "border-emerald-400 bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10"
              : "border-zinc-200 text-zinc-500 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800"
          }`}
        >
          <svg width="14" height="14" 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>
        </button>
        <button
          type="button"
          aria-label="Not helpful"
          onClick={() => setValue("down")}
          className={`flex h-7 w-7 items-center justify-center rounded-md border transition ${
            value === "down"
              ? "border-red-400 bg-red-50 text-red-600 dark:bg-red-500/10"
              : "border-zinc-200 text-zinc-500 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800"
          }`}
        >
          <svg width="14" height="14" 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>
        </button>
      </div>

      {value === "down" && (
        <div className="flex items-center gap-2">
          <input
            value={comment}
            onChange={(e) => setComment(e.target.value)}
            placeholder="What went wrong? (optional)"
            className="flex-1 rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-[13px] outline-none focus:border-zinc-400 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
          />
          <button
            type="button"
            onClick={() => submit("down", comment)}
            className="rounded-lg bg-zinc-900 px-3 py-1.5 text-[13px] font-medium text-white dark:bg-white dark:text-zinc-900"
          >
            Send
          </button>
        </div>
      )}
    </div>
  );
}