Rating widget
Collect ratings anywhere in your app, in whichever type fits the moment, and see the results per place on the dashboard.
Concepts
ratingKeynames a place you collect ratings, like"ai-response"or"app-overall". Each key has its own settings and its own results. Any key works; it doesn't need to be created first.subjectis the specific thing being rated, like one AI message. Itsmetadatalets you break results down later (for example by model).- A rating is final once submitted. Each visitor rates a subject once; ratings without a subject follow
cooldownDays.
| Prop | Type | Description |
|---|---|---|
thumbs | 0 or 1 | AI answers, quick yes/no. |
stars | 1–N (N = 3–10) | Optional half stars. |
range | 1–10 | Ten numbered buttons. |
emotion | 3 or 5 faces | Experiences, onboarding. |
Setup
Mount RatingProvider once, inside RoastnestProvider.
import { RoastnestProvider, RatingProvider } from "@roastnest/react";
<RoastnestProvider projectId="YOUR_SITE_CODE">
<RatingProvider>
<App />
</RatingProvider>
</RoastnestProvider><RatingProvider
mode="self-hosted"
onSubmit={async (rating) => {
await fetch("/api/ratings", { method: "POST", body: JSON.stringify(rating) });
return true;
}}
ratings={{
"ai-response": { variant: "thumbs", comment: { mode: "belowThreshold" } },
}}
>onSubmit receives { clientRatingId, ratingKey, variant, value, scale, subject, comment, trigger, visitorId, metadata, submittedAt }. Store scale with value — it's what makes a 4/5 comparable with a 7/10.
Three ways to ask
Inline
import { Rating } from "@roastnest/react";
function Message({ message }) {
return (
<div>
<p>{message.text}</p>
<Rating
ratingKey="ai-response"
subject={{ type: "ai_response", id: message.id, metadata: { model: message.model } }}
variant="thumbs"
layout="compact"
/>
</div>
);
}layout="compact" shows just the input; with comment: { mode: "never" } a click saves the rating right away. An already-rated subject shows its value, read-only.
After an action
import { useRating } from "@roastnest/react";
function ExportButton() {
const { requestRating } = useRating();
const onExport = async () => {
const job = await exportReport();
const result = await requestRating({
ratingKey: "export-result",
subject: { type: "export", id: job.id },
variant: "emotion",
popup: { position: "bottom-right", backdrop: "none", autoHideMs: 10000 },
});
// RatingResult, or null if dismissed, already rated, or another popup was open
};
return <button onClick={onExport}>Export</button>;
}One popup shows at a time; extra requests wait or resolve null depending on queuePolicy. Outside React use ratingController.request().
Floating button and auto popup
import { RatingTrigger } from "@roastnest/react";
<RatingTrigger ratingKey="app-overall" placement="right-center" label="Rate us" />
// Or open it by itself:
<RatingProvider
autoTriggers={[
{ ratingKey: "app-overall", pages: ["/dashboard/*"], after: { sessions: 3, timeOnPageSec: 60 } },
]}
>The button hides once the visitor has rated. Auto popups fire at most once per session and never for someone who already rated.
Configuration
Settings can come from the dashboard (Ratings → a key → Settings), the provider's ratings prop, component props, or requestRating options. Later wins: defaults ← dashboard ← ratings prop ← props / request options.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "stars" | "range" | "emotion" | "thumbs" | "stars" | Rating type. |
question / description | string | — | Heading text. |
starCount / allowHalf | number / boolean | 5 / false | Stars only. |
emotionCount / emotions | 3 | 5 / { value, emoji, label }[] | 5 | Emotion only. |
labels | { low?, high? } | — | End labels for stars and 1–10. |
comment | { mode?, threshold?, required?, placeholder?, lowPlaceholder?, maxLength? } | "belowThreshold" | Modes: never, always, afterRating, belowThreshold. Default thresholds: thumbs 0, 1–10 4, stars 2, emotion 2. |
submitLabel / skipLabel / successMessage / errorMessage | string | — | Copy. |
cooldownDays | number | — | Ratings without a subject: days before asking again. Unset = once. |
popup | { position?, backdrop?, closeOnBackdropClick?, autoHideMs? } | center, dim | Popup placement. autoHideMs pauses on hover and stops once a value is picked. |
triggerButton | { label?, placement?, mode? } | — | Defaults for <RatingTrigger>. |
<Rating> also takes layout, size, className, style, icons, onChange and onSubmitted.
Custom UI
import { useRatingState } from "@roastnest/react";
function MyThumbs({ messageId }) {
const r = useRatingState({ ratingKey: "ai-response", subject: { type: "ai_response", id: messageId } });
if (r.status === "loading") return null;
if (r.status === "success") return <span>Thanks!</span>;
return (
<>
<button onClick={() => r.setValue(1)}>👍</button>
<button onClick={() => r.setValue(0)}>👎</button>
{r.showComment && <textarea value={r.comment} onChange={(e) => r.setComment(e.target.value)} />}
<button disabled={!r.canSubmit} onClick={() => r.submit()}>Send</button>
</>
);
}In the dashboard
The Ratings page shows, per key and date range: totals, positive and negative share, average, a distribution chart, the trend over time, a breakdown by any subject.metadata field, and every response with its comment.
Who rated
A rating is linked to a person when the visitor is identified. An id is enough:
const { setUser } = useRoastnest();
setUser({ id: user.id, name: user.name }); // email is optional for ratingsOtherwise it shows as anonymous.
New ratings are also sent as the rating.created webhook.