(() =>
+ user ? user.id : null,
+ );
+ if (!initialState && user && hydratedFor !== user.id) {
+ const fromLocal = readLocalState(user.id, recId, recText);
+ setState(fromLocal);
+ setHydratedFor(user.id);
+ }
+
+ const mutation = useMutation({
+ mutationFn: async (helpful: boolean) => {
+ const res = await fetch("/api/insights/feedback", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ recommendationId: recId,
+ recommendationText: recText,
+ recommendationSeverity: recSeverity,
+ metricSourceType,
+ metricSourceTimeRange,
+ helpful,
+ }),
+ });
+ const body = (await res.json()) as FeedbackEnvelope;
+ if (res.status === 409) {
+ return { duplicate: true, helpful } as const;
+ }
+ if (!res.ok) {
+ throw new Error(body.error ?? "feedback_failed");
+ }
+ return { duplicate: false, helpful } as const;
+ },
+ onSuccess: (result) => {
+ if (!user) return;
+ if (result.duplicate) {
+ const verdict = result.helpful
+ ? "already-rated-up"
+ : "already-rated-down";
+ writeLocalState(user.id, recId, recText, verdict);
+ setState(verdict);
+ return;
+ }
+ const verdict = result.helpful ? "up" : "down";
+ writeLocalState(user.id, recId, recText, verdict);
+ setState(`submitted-${verdict}` as const);
+ queryClient.invalidateQueries({ queryKey: ["insights"] });
+ },
+ onError: () => {
+ // Optimistic-rollback: drop back to default so the user can
+ // retry. We don't surface a toast — the parent rec card already
+ // crowded enough; a silent reset is the friendlier UX here.
+ setState("default");
+ },
+ });
+
+ const submit = useMemo(
+ () => (helpful: boolean) => {
+ setState(helpful ? "submitting-up" : "submitting-down");
+ mutation.mutate(helpful);
+ },
+ [mutation],
+ );
+
+ // ── Render ──────────────────────────────────────────────────────
+
+ if (state === "submitted-up" || state === "submitted-down") {
+ const verdict = state === "submitted-up" ? "up" : "down";
+ return (
+
+
+
+ {t("insights.recommendation.feedbackThanks")}
+
+
+ );
+ }
+
+ if (state === "already-rated-up" || state === "already-rated-down") {
+ const verdict = state === "already-rated-up" ? "up" : "down";
+ return (
+
+ {verdict === "up" ? (
+
+ ) : (
+
+ )}
+
+ {t("insights.recommendation.feedbackAlreadyRated")}
+
+
+ );
+ }
+
+ const submittingUp = state === "submitting-up";
+ const submittingDown = state === "submitting-down";
+ const disabled = submittingUp || submittingDown;
+
+ return (
+
+
+
+
+ );
+}