"use client"; import { useState, useEffect, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Shield, ShieldAlert, AlertTriangle, CheckCircle2, XCircle, Info, ChevronDown, ChevronRight, Layers, Database, Server, Monitor, Lock, TestTube, FileText, Zap, RefreshCw, MessageSquare, TrendingUp, Minus, Eye, } from "lucide-react"; interface AnalysisData { id: string; status: string; healthScore: number | null; healthGrade: string | null; blastRadius: { directChanges: number; transitiveChanges: number; affectedRoutes: number; affectedComponents: number; affectedLayers: string[]; riskScore: number; riskLevel: string; summary: string; } | null; architectureImpact: { layers: Array<{ layer: string; files: string[]; changeCount: number; severity: string; }>; crossLayerChanges: boolean; affectedLayers: string[]; riskLevel: string; riskSummary: string; securityImpact: boolean; databaseImpact: boolean; } | null; changeGroups: Array<{ id: string; title: string; description: string; files: string[]; layer: string; riskLevel: string; totalAdditions: number; totalDeletions: number; }> | null; complexityData: { cyclomaticComplexity: number; nestingDepth: number; longFunctions: Array<{ name: string; lines: number }>; riskLevel: string; } | null; summaries: any; filesAnalyzed: number; totalAdditions: number; totalDeletions: number; model: string | null; provider: string | null; errorMessage: string | null; createdAt: string; completedAt: string | null; } interface InlineComment { id: string; filePath: string; line: number; severity: string; type: string; category: string; title: string; message: string; suggestedFix: string | null; explanation: string | null; confidence: number | null; isResolved: number | null; isApplied: number | null; } interface Props { owner: string; repo: string; prNumber: number; } const LAYER_ICONS: Record> = { database: Database, backend: Server, frontend: Monitor, infrastructure: Layers, security: Lock, testing: TestTube, documentation: FileText, config: FileText, }; function getSeverityConfig(severity: string) { switch (severity) { case "critical": case "error": return { color: "text-red-500", bg: "bg-red-500/10", icon: XCircle }; case "warning": case "medium": return { color: "text-yellow-500", bg: "bg-yellow-500/10", icon: AlertTriangle }; case "info": return { color: "text-blue-500", bg: "bg-blue-500/10", icon: Info }; case "low": return { color: "text-green-500", bg: "bg-green-500/10", icon: CheckCircle2 }; default: return { color: "text-muted-foreground", bg: "bg-muted", icon: Info }; } } function getGradeColor(grade: string | null) { switch (grade) { case "A+": case "A": return "text-green-500"; case "B": return "text-blue-500"; case "C": return "text-yellow-500"; case "D": return "text-orange-500"; case "F": return "text-red-500"; default: return "text-muted-foreground"; } } function TabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) { return ( ); } export default function CodeReviewPanel({ owner, repo, prNumber }: Props) { const [analysis, setAnalysis] = useState(null); const [comments, setComments] = useState([]); const [loading, setLoading] = useState(true); const [analyzing, setAnalyzing] = useState(false); const [expandedGroups, setExpandedGroups] = useState>(new Set()); const [activeTab, setActiveTab] = useState<"overview" | "groups" | "comments">("overview"); const fetchAnalysis = useCallback(async () => { try { const res = await fetch(`/api/repos/${owner}/${repo}/pulls/${prNumber}/analysis`); const data = await res.json(); if (data.data) { setAnalysis(data.data.analysis); setComments(data.data.inlineComments || []); } } catch (err) { console.error("Failed to fetch analysis:", err); } finally { setLoading(false); } }, [owner, repo, prNumber]); const triggerAnalysis = useCallback(async () => { setAnalyzing(true); try { const res = await fetch(`/api/repos/${owner}/${repo}/pulls/${prNumber}/analysis`, { method: "POST", }); const data = await res.json(); if (data.data?.status === "running") { const pollInterval = setInterval(async () => { const pollRes = await fetch(`/api/repos/${owner}/${repo}/pulls/${prNumber}/analysis`); const pollData = await pollRes.json(); if (pollData.data?.analysis?.status === "completed" || pollData.data?.analysis?.status === "failed") { clearInterval(pollInterval); await fetchAnalysis(); setAnalyzing(false); } }, 3000); } } catch (err) { console.error("Failed to trigger analysis:", err); setAnalyzing(false); } }, [owner, repo, prNumber, fetchAnalysis]); useEffect(() => { fetchAnalysis(); }, [fetchAnalysis]); useEffect(() => { if (analysis?.status === "running") { const interval = setInterval(fetchAnalysis, 5000); return () => clearInterval(interval); } }, [analysis?.status, fetchAnalysis]); const toggleGroup = (id: string) => { setExpandedGroups(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; if (loading) { return (
Loading analysis...
); } if (!analysis) { return (

No Analysis Yet

Run an AI-powered analysis to get code review insights.

); } const gradeColor = getGradeColor(analysis.healthGrade); const riskConfig = getSeverityConfig(analysis.blastRadius?.riskLevel || "low"); const RiskIcon = riskConfig.icon; return (
{/* Health Score Header */}
{analysis.healthScore || "—"}
{analysis.healthGrade || "—"}

PR Health Score

{analysis.filesAnalyzed} file(s) analyzed • +{analysis.totalAdditions}/-{analysis.totalDeletions}

{/* Risk Badge */}
{analysis.blastRadius?.riskLevel?.charAt(0).toUpperCase()}{analysis.blastRadius?.riskLevel?.slice(1)} Risk
{/* Blast Radius */} {analysis.blastRadius && (
{analysis.blastRadius.directChanges}
Files Changed
{analysis.blastRadius.affectedRoutes}
Routes Affected
{analysis.blastRadius.affectedComponents}
Components
)} {analysis.blastRadius?.summary && (

{analysis.blastRadius.summary}

)}
{/* Tab Navigation */}
setActiveTab("overview")}> Overview setActiveTab("groups")}> Groups setActiveTab("comments")}> Comments {comments.length > 0 && ( {comments.length} )}
{/* Overview Tab */} {activeTab === "overview" && (
{/* Architecture Impact */} {analysis.architectureImpact && (

Architecture Impact

{analysis.architectureImpact.layers.map((layer, i) => { const LayerIcon = LAYER_ICONS[layer.layer] || Layers; const sevColor = layer.severity === "high" ? "bg-red-500/10 text-red-500" : layer.severity === "medium" ? "bg-yellow-500/10 text-yellow-500" : "bg-green-500/10 text-green-500"; return (
{layer.layer}
{layer.changeCount} file(s) {layer.severity}
); })}
{analysis.architectureImpact.crossLayerChanges && (
Changes span multiple architectural layers — review carefully
)} {analysis.architectureImpact.securityImpact && (
Security-related files modified
)}
)} {/* Complexity */} {analysis.complexityData && (

Complexity Analysis

{analysis.complexityData.cyclomaticComplexity}
Cyclomatic Complexity
{analysis.complexityData.nestingDepth}
Max Nesting Depth
{analysis.complexityData.longFunctions.length > 0 && (

Long Functions:

{analysis.complexityData.longFunctions.map((fn, i) => (
{fn.name} ({fn.lines} lines)
))}
)}
)} {/* AI Summary */} {analysis.summaries?.overall && (

AI Summary

{analysis.summaries.overall.summary}

{analysis.summaries.overall.keyChanges?.length > 0 && (
    {analysis.summaries.overall.keyChanges.map((change: string, i: number) => (
  • {change}
  • ))}
)}
)} {/* AI Provider Info */} {analysis.provider && (
Analyzed by {analysis.provider}{analysis.model ? ` (${analysis.model})` : ""}
)}
)} {/* Groups Tab */} {activeTab === "groups" && (
{analysis.changeGroups && analysis.changeGroups.length > 0 ? ( analysis.changeGroups.map(group => { const isExpanded = expandedGroups.has(group.id); const groupRisk = getSeverityConfig(group.riskLevel); const GroupRiskIcon = groupRisk.icon; const LayerIcon = LAYER_ICONS[group.layer] || Layers; return (
{isExpanded && (
{group.files.map(file => (
{file}
))}
)}
); }) ) : (

No change groups available

)}
)} {/* Comments Tab */} {activeTab === "comments" && (
{comments.length > 0 ? ( comments.map(comment => { const cfg = getSeverityConfig(comment.severity); const CommentIcon = cfg.icon; return (
{comment.title}
{comment.severity}

{comment.message}

{comment.filePath}:{comment.line} • {comment.type} {comment.confidence != null && ( <> • {comment.confidence}% confidence )}
{comment.suggestedFix && (
{comment.suggestedFix}
)}
); }) ) : (

No inline comments yet

)}
)} {/* Error State */} {analysis.status === "failed" && analysis.errorMessage && (
Analysis failed: {analysis.errorMessage}
)} {/* Running State */} {analysis.status === "running" && (
Analysis in progress...
)}
); }