UI kit
FeedbackBar
FeedbackReact ยท Next.jsCollects 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.localvars).
Usage
usage.tsx
import { FeedbackBar } from "@/components/FeedbackBar";
export default function Example() {
return (
<FeedbackBar endpoint="https://api.example.com/feedback" messageId={id} />
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
endpoint | string | โ | POST { messageId, value, comment } to collect feedback. |
messageId | string | โ | Identifier for the rated message. |
onFeedback | (value, comment?) => void | โ | Local callback when feedback is submitted. |
apiKey | string | โ | Sent as X-API-Key when your endpoint requires it. |
fetcher | typeof 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>
);
}
1 ยท Add to .env.local
RAG_FEEDBACK_ENDPOINT=
RAG_FEEDBACK_ENDPOINTโ Optional โ forward feedback to your pipeline/analytics
Feedback POSTs to a Route Handler so you can attach trusted metadata (user id, timestamp) and persist it server-side.
2 ยท Component
components/FeedbackBar.tsx
"use client";
import { useState } from "react";
interface FeedbackBarProps {
/** Local route handler that records feedback. Default: "/api/feedback". */
route?: string;
messageId?: string;
onFeedback?: (value: "up" | "down", comment?: string) => void;
className?: string;
}
/**
* FeedbackBar (Next.js) โ ๐/๐ rating that POSTs to a local Route Handler
* (app/api/feedback/route.ts), so you can attach trusted metadata server-side
* and persist it wherever you like.
*/
export function FeedbackBar({ route = "/api/feedback", messageId, onFeedback, 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);
try {
await fetch(route, {
method: "POST",
headers: { "Content-Type": "application/json" },
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>
);
}
3 ยท Route handler
app/api/feedback/route.ts
// app/api/feedback/route.ts
//
// Receives answer feedback from the browser. Persist it however you like
// (database, analytics, queue) and/or forward it to your pipeline. Keeping
// this server-side means you can attach trusted metadata (user id, timestamp)
// the client can't forge.
export const dynamic = "force-dynamic";
export async function POST(req: Request) {
const body = (await req.json()) as {
messageId?: string;
value: "up" | "down";
comment?: string | null;
};
// TODO: persist `body` to your store. Example: await db.feedback.insert(...).
console.log("[feedback]", body);
// Optionally forward to the pipeline / an analytics endpoint.
if (process.env.RAG_FEEDBACK_ENDPOINT) {
try {
await fetch(process.env.RAG_FEEDBACK_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(process.env.RAG_API_KEY ? { "X-API-Key": process.env.RAG_API_KEY } : {}),
},
body: JSON.stringify({ ...body, at: new Date().toISOString() }),
});
} catch {
/* swallow โ feedback is best-effort */
}
}
return Response.json({ ok: true });
}