Rating widget

Collect ratings anywhere in your app, in whichever type fits the moment, and see the results per place on the dashboard.

Concepts

  • ratingKey names 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.
  • subject is the specific thing being rated, like one AI message. Its metadata lets 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.
PropTypeDescription
thumbs0 or 1AI answers, quick yes/no.
stars1–N (N = 3–10)Optional half stars.
range1–10Ten numbered buttons.
emotion3 or 5 facesExperiences, onboarding.

Setup

Mount RatingProvider once, inside RoastnestProvider.

Cloud mode
import { RoastnestProvider, RatingProvider } from "@roastnest/react";

<RoastnestProvider projectId="YOUR_SITE_CODE">
  <RatingProvider>
    <App />
  </RatingProvider>
</RoastnestProvider>
Self-hosted mode
<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

Under each AI answer
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

Popup
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

Global
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.

PropTypeDefaultDescription
variant"stars" | "range" | "emotion" | "thumbs""stars"Rating type.
question / descriptionstringHeading text.
starCount / allowHalfnumber / boolean5 / falseStars only.
emotionCount / emotions3 | 5 / { value, emoji, label }[]5Emotion 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 / errorMessagestringCopy.
cooldownDaysnumberRatings without a subject: days before asking again. Unset = once.
popup{ position?, backdrop?, closeOnBackdropClick?, autoHideMs? }center, dimPopup 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

useRatingState
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 ratings

Otherwise it shows as anonymous.

New ratings are also sent as the rating.created webhook.