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

@ -1,6 +1,13 @@
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { DocumentRevision, Issue, IssueDocument } from "@paperclipai/shared";
import type {
DocumentRevision,
FeedbackDataSharingPreference,
FeedbackVote,
FeedbackVoteValue,
Issue,
IssueDocument,
} from "@paperclipai/shared";
import { useLocation } from "@/lib/router";
import { ApiError } from "../api/client";
import { issuesApi } from "../api/issues";
@ -9,6 +16,7 @@ import { queryKeys } from "../lib/queryKeys";
import { cn, relativeTime } from "../lib/utils";
import { MarkdownBody } from "./MarkdownBody";
import { MarkdownEditor, type MentionOption } from "./MarkdownEditor";
import { OutputFeedbackButtons } from "./OutputFeedbackButtons";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@ -101,14 +109,26 @@ function documentHasUnsavedChanges(doc: IssueDocument, draft: DraftState | null)
export function IssueDocumentsSection({
issue,
canDeleteDocuments,
feedbackVotes = [],
feedbackDataSharingPreference = "prompt",
feedbackTermsUrl = null,
mentions,
imageUploadHandler,
onVote,
extraActions,
}: {
issue: Issue;
canDeleteDocuments: boolean;
feedbackVotes?: FeedbackVote[];
feedbackDataSharingPreference?: FeedbackDataSharingPreference;
feedbackTermsUrl?: string | null;
mentions?: MentionOption[];
imageUploadHandler?: (file: File) => Promise<string>;
onVote?: (
revisionId: string,
vote: FeedbackVoteValue,
options?: { allowSharing?: boolean; reason?: string },
) => Promise<void>;
extraActions?: ReactNode;
}) {
const queryClient = useQueryClient();
@ -207,6 +227,15 @@ export function IssueDocumentsSection({
});
}, [documents]);
const feedbackVoteByTargetId = useMemo(() => {
const map = new Map<string, FeedbackVoteValue>();
for (const feedbackVote of feedbackVotes) {
if (feedbackVote.targetType !== "issue_document_revision") continue;
map.set(feedbackVote.targetId, feedbackVote.vote);
}
return map;
}, [feedbackVotes]);
const hasRealPlan = sortedDocuments.some((doc) => doc.key === "plan");
const isEmpty = sortedDocuments.length === 0 && !issue.legacyPlanDocument;
const newDocumentKeyError =
@ -718,6 +747,7 @@ export function IssueDocumentsSection({
const displayedRevisionNumber = selectedHistoricalRevision?.revisionNumber ?? doc.latestRevisionNumber;
const displayedUpdatedAt = selectedHistoricalRevision?.createdAt ?? doc.updatedAt;
const showTitle = !isPlanKey(doc.key) && !!displayedTitle.trim() && !titlesMatchKey(displayedTitle, doc.key);
const canVoteOnDocument = Boolean(doc.latestRevisionId && doc.updatedByAgentId && !doc.updatedByUserId && onVote);
return (
<div
@ -1053,6 +1083,16 @@ export function IssueDocumentsSection({
: ""}
</span>
</div>
{canVoteOnDocument && doc.latestRevisionId ? (
<OutputFeedbackButtons
activeVote={feedbackVoteByTargetId.get(doc.latestRevisionId) ?? null}
sharingPreference={feedbackDataSharingPreference}
termsUrl={feedbackTermsUrl}
onVote={(vote: FeedbackVoteValue, options?: { allowSharing?: boolean; reason?: string }) =>
onVote?.(doc.latestRevisionId!, vote, options) ?? Promise.resolve()
}
/>
) : null}
</div>
) : null}