import { DiffView } from "@/components/diff/DiffView"; import { useCallback, useState, useEffect } from "react"; import { toast } from "sonner"; interface InlineDiffReviewProps { rawDiff: string; owner: string; repo: string; pullNumber: number; repoUrl: string; currentUser?: any; baseSha?: string; headSha?: string; } /** * Wrapper around DiffView that adds inline comment functionality for PR reviews. * Shows a comment form when user clicks the "+" button on a diff line. */ export function InlineDiffReview({ rawDiff, owner, repo, pullNumber, repoUrl, currentUser, headSha, }: InlineDiffReviewProps) { const [commentForm, setCommentForm] = useState<{ filePath: string; line: number; side: "LEFT" | "RIGHT"; } | null>(null); const [commentBody, setCommentBody] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [suggestionMode, setSuggestionMode] = useState(false); // Partial File Approvals state const [fileApprovals, setFileApprovals] = useState>({}); // Code Quality state const [codeQualityIssues, setCodeQualityIssues] = useState>([]); const fetchApprovals = useCallback(async () => { if (!currentUser) return; try { const res = await fetch(`/api/repos/${owner}/${repo}/pulls/${pullNumber}/file-approvals`); if (res.ok) { const data = await res.json(); const approvalMap: Record = {}; for (const approval of data.approvals || []) { if (approval.approvers?.includes(currentUser.username) || approval.approvedById === currentUser.id) { approvalMap[approval.path] = true; } } setFileApprovals(approvalMap); } } catch {} }, [owner, repo, pullNumber, currentUser]); const fetchCodeQuality = useCallback(async () => { try { const res = await fetch(`/api/repos/${owner}/${repo}/pulls/${pullNumber}/code-quality`); if (res.ok) { const data = await res.json(); setCodeQualityIssues(data); } } catch {} }, [owner, repo, pullNumber]); useEffect(() => { fetchApprovals(); fetchCodeQuality(); }, [fetchApprovals, fetchCodeQuality]); const handleApproveFile = useCallback(async (filePath: string, approved: boolean) => { try { if (approved) { // Create approval await fetch(`/api/repos/${owner}/${repo}/pulls/${pullNumber}/file-approvals`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: filePath, commitSha: headSha || "unknown" }) }); setFileApprovals(prev => ({ ...prev, [filePath]: true })); } else { await fetch(`/api/repos/${owner}/${repo}/pulls/${pullNumber}/file-approvals?path=${encodeURIComponent(filePath)}`, { method: "DELETE" }); setFileApprovals(prev => ({ ...prev, [filePath]: false })); } } catch { toast.error("Failed to toggle approval"); } }, [owner, repo, pullNumber, headSha, currentUser]); const handleAddComment = useCallback( (filePath: string, line: number, side: "LEFT" | "RIGHT") => { setCommentForm({ filePath, line, side }); setCommentBody(""); setSuggestionMode(false); }, [], ); const handleCancel = useCallback(() => { setCommentForm(null); setCommentBody(""); setSuggestionMode(false); }, []); const handleSubmit = useCallback(async () => { if (!commentForm || !commentBody.trim()) return; setIsSubmitting(true); try { const payload: any = { body: commentBody, path: commentForm.filePath, line: commentForm.line, side: commentForm.side, }; if (headSha) { payload.commitSha = headSha; } // Check for suggestion syntax if ( commentBody.includes("```suggestion") || commentBody.includes("```suggestion\n") ) { const match = commentBody.match(/```suggestion\n([\s\S]*?)```/); if (match) { payload.suggestionContent = match[1]; } } const res = await fetch( `/api/repos/${owner}/${repo}/pulls/${pullNumber}/comments`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, ); if (res.ok) { toast.success("Comment added"); setCommentForm(null); setCommentBody(""); } else { const err = await res.json(); toast.error(err.error || "Failed to add comment"); } } catch { toast.error("Failed to add comment"); } finally { setIsSubmitting(false); } }, [commentForm, commentBody, owner, repo, pullNumber, headSha]); const insertSuggestion = useCallback(() => { setSuggestionMode(true); setCommentBody( (prev) => prev + (prev ? "\n" : "") + "```suggestion\n// Replace with your suggested code\n```", ); }, []); return (
{/* Floating inline comment form */} {commentForm && (
Add comment on{" "} {commentForm.filePath} line {commentForm.line} ({commentForm.side})