import { IconCheck, IconX, IconClock } from "@tabler/icons-react"; import { Badge } from "@kandev/ui/badge"; import type { PRReview, RequestedReviewer } from "@/lib/types/github"; import { CollapsibleSection, FeedbackItemRow } from "./pr-shared"; function ReviewStateIcon({ state }: { state: string }) { if (state === "APPROVED") return ; if (state === "CHANGES_REQUESTED") return ; return ; } function reviewStateLabel(state: string): string { const labels: Record = { APPROVED: "Approved", CHANGES_REQUESTED: "Changes Requested", COMMENTED: "Commented", PENDING: "Pending", DISMISSED: "Dismissed", }; return labels[state] ?? state; } export function ReviewStateBadge({ state }: { state: string }) { if (!state) return null; const styles: Record = { approved: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400", changes_requested: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400", pending: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400", }; const labelMap: Record = { approved: "Approved", changes_requested: "Changes requested", pending: "Review pending", }; return ( {labelMap[state] ?? state} ); } function buildReviewMessage(review: PRReview, prUrl: string): string { const parts = [`Review from **${review.author}** (${review.state}):`]; if (review.body) parts.push(review.body); parts.push(`PR: ${prUrl}`); parts.push("Please address this review feedback."); return parts.join("\n\n"); } function buildAllReviewsMessage(reviews: PRReview[], prUrl: string): string { const parts = ["### All PR Reviews", ""]; for (const r of reviews) { parts.push(`**${r.author}** — ${reviewStateLabel(r.state)}`); if (r.body) parts.push(r.body); parts.push(""); } parts.push(`PR: ${prUrl}`); parts.push("Please address the review feedback above."); return parts.join("\n"); } function deduplicateReviews(reviews: PRReview[]): PRReview[] { const latestByAuthor = new Map(); for (const review of reviews) { const current = latestByAuthor.get(review.author); if (!current || new Date(review.created_at) > new Date(current.created_at)) { latestByAuthor.set(review.author, review); } } return Array.from(latestByAuthor.values()); } function formatPendingReviewer(reviewer: RequestedReviewer): string { if (reviewer.type === "team") return `${reviewer.login} (team)`; return reviewer.login; } function formatReviewSummary(reviews: PRReview[]): string { const approved = reviews.filter((r) => r.state === "APPROVED").length; const changes = reviews.filter((r) => r.state === "CHANGES_REQUESTED").length; const parts: string[] = []; if (approved > 0) parts.push(`${approved} approved`); if (changes > 0) parts.push(`${changes} changes requested`); const other = reviews.length - approved - changes; if (other > 0) parts.push(`${other} other`); return parts.join(", "); } function computeSectionSummary( reviews: PRReview[], requestedReviewers: RequestedReviewer[], pendingReviewCount: number, ) { const submittedSummary = reviews.length > 0 ? formatReviewSummary(reviews) : ""; const pendingCount = requestedReviewers.length > 0 ? requestedReviewers.length : pendingReviewCount; const summaryParts: string[] = []; if (submittedSummary) summaryParts.push(submittedSummary); if (pendingCount > 0) summaryParts.push(`${pendingCount} pending`); const summary = summaryParts.length > 0 ? ` \u2014 ${summaryParts.join(", ")}` : ""; return { pendingCount, summary, totalCount: reviews.length + pendingCount }; } function ReviewMetaBadge({ state }: { state: string }) { return ( <> {reviewStateLabel(state)} > ); } function SubmittedReviewRow({ review, prUrl, onAddAsContext, }: { review: PRReview; prUrl: string; onAddAsContext: (message: string) => void; }) { const hasActionable = review.state === "CHANGES_REQUESTED" || !!review.body; return ( } onAddAsContext={ hasActionable ? () => onAddAsContext(buildReviewMessage(review, prUrl)) : undefined } /> ); } function PendingReviewRow({ reviewer }: { reviewer: RequestedReviewer }) { return ( {formatPendingReviewer(reviewer)} Pending review ); } export function ReviewsSection({ reviews, requestedReviewers, prUrl, reviewState, pendingReviewCount, onAddAsContext, }: { reviews: PRReview[]; requestedReviewers: RequestedReviewer[]; prUrl: string; reviewState: string; pendingReviewCount: number; onAddAsContext: (message: string) => void; }) { const dedupedReviews = deduplicateReviews(reviews); const reviewedAuthors = new Set( dedupedReviews.filter((r) => r.state !== "PENDING").map((r) => r.author), ); const pendingOnly = requestedReviewers.filter((r) => !reviewedAuthors.has(r.login)); const { pendingCount, summary, totalCount } = computeSectionSummary( dedupedReviews, pendingOnly, pendingReviewCount, ); const pendingText = pendingCount > 0 ? ` (${pendingCount} pending)` : ""; const subtitle = reviewState ? ( Overall: {pendingText && {pendingText}} ) : null; return ( 0 ? () => onAddAsContext(buildAllReviewsMessage(dedupedReviews, prUrl)) : undefined } addAllLabel="Add all reviews to chat context" > {dedupedReviews.length === 0 && pendingCount === 0 && ( No reviews yet )} {dedupedReviews.map((review) => ( ))} {pendingOnly.map((reviewer) => ( ))} ); }
No reviews yet