import { IconCheck, IconX, IconClock } from "@tabler/icons-react"; import { Badge } from "@kandev/ui/badge"; import { Button } from "@kandev/ui/button"; import type { PRReview, RequestedReviewer } from "@/lib/types/github"; import { CollapsibleSection, FeedbackItemRow } from "./pr-shared"; import { useTranslation } from "react-i18next"; 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 normalizeLogin(login: string): string { return login.trim().toLowerCase(); } export function reconcileReviews(reviews: PRReview[], requestedReviewers: RequestedReviewer[]) { const latestByAuthor = new Map(); for (const review of reviews) { const author = normalizeLogin(review.author); const current = latestByAuthor.get(author); if (!current || new Date(review.created_at) > new Date(current.created_at)) { latestByAuthor.set(author, review); } } const requestedByLogin = new Map(); for (const reviewer of requestedReviewers) { const login = normalizeLogin(reviewer.login); if (!requestedByLogin.has(login)) requestedByLogin.set(login, reviewer); } return { reviews: Array.from(latestByAuthor.entries()) .filter(([author]) => !requestedByLogin.has(author)) .map(([, review]) => review), pendingReviewers: Array.from(requestedByLogin.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 ? ` - ${summaryParts.join(", ")}` : ""; return { pendingCount, summary, totalCount: reviews.length + pendingCount }; } function ReviewMetaBadge({ state }: { state: string }) { return ( <> {reviewStateLabel(state)} ); } function SubmittedReviewRow({ review, prUrl, onAddAsContext, canReRequest, isRequesting, onReRequest, }: { review: PRReview; prUrl: string; onAddAsContext: (message: string) => void; canReRequest: boolean; isRequesting: boolean; onReRequest?: (reviewer: string) => void; }) { const { t } = useTranslation(); const hasActionable = review.state === "CHANGES_REQUESTED" || !!review.body; return (
} trailingAction={ canReRequest && review.state === "DISMISSED" && onReRequest ? (
) : undefined } onAddAsContext={ hasActionable ? () => onAddAsContext(buildReviewMessage(review, prUrl)) : undefined } />
); } function PendingReviewRow({ reviewer }: { reviewer: RequestedReviewer }) { const { t } = useTranslation(); return (
{formatPendingReviewer(reviewer)} {t("github:pendingReview")}
); } export function ReviewsSection({ reviews, requestedReviewers, prUrl, reviewState, pendingReviewCount, onAddAsContext, canReRequest = false, requestingReviewers = [], onReRequest, }: { reviews: PRReview[]; requestedReviewers: RequestedReviewer[]; prUrl: string; reviewState: string; pendingReviewCount: number; onAddAsContext: (message: string) => void; canReRequest?: boolean; requestingReviewers?: string[]; onReRequest?: (reviewer: string) => void; }) { const { t } = useTranslation(); const { reviews: reconciledReviews, pendingReviewers } = reconcileReviews( reviews, requestedReviewers, ); const requestingReviewerLogins = new Set(requestingReviewers.map(normalizeLogin)); const { pendingCount, summary, totalCount } = computeSectionSummary( reconciledReviews, pendingReviewers, pendingReviewCount, ); const pendingText = pendingCount > 0 ? ` (${pendingCount} pending)` : ""; const subtitle = reviewState ? (
{t("github:overallLabel")} {pendingText && {pendingText}}
) : null; return ( 0 ? () => onAddAsContext(buildAllReviewsMessage(reconciledReviews, prUrl)) : undefined } addAllLabel={t("github:addAllReviewsToChatContext")} > {reconciledReviews.length === 0 && pendingCount === 0 && (

{t("github:noReviewsYet")}

)} {reconciledReviews.map((review) => ( ))} {pendingReviewers.map((reviewer) => ( ))}
); }