Add feedback voting and thumbs capture flow

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-02 09:11:49 -05:00
parent 3db6bdfc3c
commit c0d0d03bce
66 changed files with 18988 additions and 78 deletions

View file

@ -6,6 +6,8 @@ import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { queryKeys } from "../lib/queryKeys";
import { cn } from "../lib/utils";
const FEEDBACK_TERMS_URL = import.meta.env.VITE_FEEDBACK_TERMS_URL?.trim() || "https://paperclip.ing/tos";
export function InstanceGeneralSettings() {
const { setBreadcrumbs } = useBreadcrumbs();
const queryClient = useQueryClient();
@ -23,9 +25,8 @@ export function InstanceGeneralSettings() {
queryFn: () => instanceSettingsApi.getGeneral(),
});
const toggleMutation = useMutation({
mutationFn: async (enabled: boolean) =>
instanceSettingsApi.updateGeneral({ censorUsernameInLogs: enabled }),
const updateGeneralMutation = useMutation({
mutationFn: instanceSettingsApi.updateGeneral,
onSuccess: async () => {
setActionError(null);
await queryClient.invalidateQueries({ queryKey: queryKeys.instance.generalSettings });
@ -50,6 +51,7 @@ export function InstanceGeneralSettings() {
}
const censorUsernameInLogs = generalQuery.data?.censorUsernameInLogs === true;
const feedbackDataSharingPreference = generalQuery.data?.feedbackDataSharingPreference ?? "prompt";
return (
<div className="max-w-4xl space-y-6">
@ -83,12 +85,16 @@ export function InstanceGeneralSettings() {
type="button"
data-slot="toggle"
aria-label="Toggle username log censoring"
disabled={toggleMutation.isPending}
disabled={updateGeneralMutation.isPending}
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60",
censorUsernameInLogs ? "bg-green-600" : "bg-muted",
)}
onClick={() => toggleMutation.mutate(!censorUsernameInLogs)}
onClick={() =>
updateGeneralMutation.mutate({
censorUsernameInLogs: !censorUsernameInLogs,
})
}
>
<span
className={cn(
@ -99,6 +105,82 @@ export function InstanceGeneralSettings() {
</button>
</div>
</section>
<section className="rounded-xl border border-border bg-card p-5">
<div className="space-y-4">
<div className="space-y-1.5">
<h2 className="text-sm font-semibold">AI feedback sharing</h2>
<p className="max-w-2xl text-sm text-muted-foreground">
Control whether thumbs up and thumbs down votes can send the voted AI output to
Paperclip Labs. Votes are always saved locally.
</p>
{FEEDBACK_TERMS_URL ? (
<a
href={FEEDBACK_TERMS_URL}
target="_blank"
rel="noreferrer"
className="inline-flex text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
>
Read our terms of service
</a>
) : null}
</div>
{feedbackDataSharingPreference === "prompt" ? (
<div className="rounded-lg border border-border/70 bg-accent/20 px-3 py-2 text-sm text-muted-foreground">
No default is saved yet. The next thumbs up or thumbs down choice will ask once and
then save the answer here.
</div>
) : null}
<div className="flex flex-wrap gap-2">
{[
{
value: "allowed",
label: "Always allow",
description: "Share voted AI outputs automatically.",
},
{
value: "not_allowed",
label: "Don't allow",
description: "Keep voted AI outputs local only.",
},
].map((option) => {
const active = feedbackDataSharingPreference === option.value;
return (
<button
key={option.value}
type="button"
disabled={updateGeneralMutation.isPending}
className={cn(
"rounded-lg border px-3 py-2 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60",
active
? "border-foreground bg-accent text-foreground"
: "border-border bg-background hover:bg-accent/50",
)}
onClick={() =>
updateGeneralMutation.mutate({
feedbackDataSharingPreference: option.value as
| "allowed"
| "not_allowed",
})
}
>
<div className="text-sm font-medium">{option.label}</div>
<div className="text-xs text-muted-foreground">
{option.description}
</div>
</button>
);
})}
</div>
<p className="text-xs text-muted-foreground">
To retest the first-use prompt in local dev, remove the{" "}
<code>feedbackDataSharingPreference</code> key from the{" "}
<code>instance_settings.general</code> JSON row for this instance, or set it back to{" "}
<code>"prompt"</code>. Unset and <code>"prompt"</code> both mean no default has been
chosen yet.
</p>
</div>
</section>
</div>
);
}