✨ feat: improve CodeReviewPanel.tsx
swadhinbiswas committed 3 months ago
d0e310a
1 parent 4fae0b9
commit d0e310a
1 file changed
+597 -0
Showing 1 changed file+597-0
▼src/components/pr/CodeReviewPanel.tsx
+597-0
| @@ -0,0 +1,597 @@ | ||
1 | +"use client"; | |
2 | + | |
3 | +import { useState, useEffect, useCallback } from "react"; | |
4 | +import { motion, AnimatePresence } from "framer-motion"; | |
5 | +import { | |
6 | + Shield, | |
7 | + ShieldAlert, | |
8 | + AlertTriangle, | |
9 | + CheckCircle2, | |
10 | + XCircle, | |
11 | + Info, | |
12 | + ChevronDown, | |
13 | + ChevronRight, | |
14 | + Layers, | |
15 | + Database, | |
16 | + Server, | |
17 | + Monitor, | |
18 | + Lock, | |
19 | + TestTube, | |
20 | + FileText, | |
21 | + Zap, | |
22 | + RefreshCw, | |
23 | + MessageSquare, | |
24 | + TrendingUp, | |
25 | + Minus, | |
26 | + Eye, | |
27 | +} from "lucide-react"; | |
28 | + | |
29 | +interface AnalysisData { | |
30 | + id: string; | |
31 | + status: string; | |
32 | + healthScore: number | null; | |
33 | + healthGrade: string | null; | |
34 | + blastRadius: { | |
35 | + directChanges: number; | |
36 | + transitiveChanges: number; | |
37 | + affectedRoutes: number; | |
38 | + affectedComponents: number; | |
39 | + affectedLayers: string[]; | |
40 | + riskScore: number; | |
41 | + riskLevel: string; | |
42 | + summary: string; | |
43 | + } | null; | |
44 | + architectureImpact: { | |
45 | + layers: Array<{ | |
46 | + layer: string; | |
47 | + files: string[]; | |
48 | + changeCount: number; | |
49 | + severity: string; | |
50 | + }>; | |
51 | + crossLayerChanges: boolean; | |
52 | + affectedLayers: string[]; | |
53 | + riskLevel: string; | |
54 | + riskSummary: string; | |
55 | + securityImpact: boolean; | |
56 | + databaseImpact: boolean; | |
57 | + } | null; | |
58 | + changeGroups: Array<{ | |
59 | + id: string; | |
60 | + title: string; | |
61 | + description: string; | |
62 | + files: string[]; | |
63 | + layer: string; | |
64 | + riskLevel: string; | |
65 | + totalAdditions: number; | |
66 | + totalDeletions: number; | |
67 | + }> | null; | |
68 | + complexityData: { | |
69 | + cyclomaticComplexity: number; | |
70 | + nestingDepth: number; | |
71 | + longFunctions: Array<{ name: string; lines: number }>; | |
72 | + riskLevel: string; | |
73 | + } | null; | |
74 | + summaries: any; | |
75 | + filesAnalyzed: number; | |
76 | + totalAdditions: number; | |
77 | + totalDeletions: number; | |
78 | + model: string | null; | |
79 | + provider: string | null; | |
80 | + errorMessage: string | null; | |
81 | + createdAt: string; | |
82 | + completedAt: string | null; | |
83 | +} | |
84 | + | |
85 | +interface InlineComment { | |
86 | + id: string; | |
87 | + filePath: string; | |
88 | + line: number; | |
89 | + severity: string; | |
90 | + type: string; | |
91 | + category: string; | |
92 | + title: string; | |
93 | + message: string; | |
94 | + suggestedFix: string | null; | |
95 | + explanation: string | null; | |
96 | + confidence: number | null; | |
97 | + isResolved: number | null; | |
98 | + isApplied: number | null; | |
99 | +} | |
100 | + | |
101 | +interface Props { | |
102 | + owner: string; | |
103 | + repo: string; | |
104 | + prNumber: number; | |
105 | +} | |
106 | + | |
107 | +const LAYER_ICONS: Record<string, React.ComponentType<any>> = { | |
108 | + database: Database, | |
109 | + backend: Server, | |
110 | + frontend: Monitor, | |
111 | + infrastructure: Layers, | |
112 | + security: Lock, | |
113 | + testing: TestTube, | |
114 | + documentation: FileText, | |
115 | + config: FileText, | |
116 | +}; | |
117 | + | |
118 | +function getSeverityConfig(severity: string) { | |
119 | + switch (severity) { | |
120 | + case "critical": | |
121 | + case "error": | |
122 | + return { color: "text-red-500", bg: "bg-red-500/10", icon: XCircle }; | |
123 | + case "warning": | |
124 | + case "medium": | |
125 | + return { color: "text-yellow-500", bg: "bg-yellow-500/10", icon: AlertTriangle }; | |
126 | + case "info": | |
127 | + return { color: "text-blue-500", bg: "bg-blue-500/10", icon: Info }; | |
128 | + case "low": | |
129 | + return { color: "text-green-500", bg: "bg-green-500/10", icon: CheckCircle2 }; | |
130 | + default: | |
131 | + return { color: "text-muted-foreground", bg: "bg-muted", icon: Info }; | |
132 | + } | |
133 | +} | |
134 | + | |
135 | +function getGradeColor(grade: string | null) { | |
136 | + switch (grade) { | |
137 | + case "A+": | |
138 | + case "A": | |
139 | + return "text-green-500"; | |
140 | + case "B": | |
141 | + return "text-blue-500"; | |
142 | + case "C": | |
143 | + return "text-yellow-500"; | |
144 | + case "D": | |
145 | + return "text-orange-500"; | |
146 | + case "F": | |
147 | + return "text-red-500"; | |
148 | + default: | |
149 | + return "text-muted-foreground"; | |
150 | + } | |
151 | +} | |
152 | + | |
153 | +function TabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) { | |
154 | + return ( | |
155 | + <button | |
156 | + onClick={onClick} | |
157 | + className={`flex-1 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${ | |
158 | + active | |
159 | + ? "bg-primary text-primary-foreground" | |
160 | + : "text-muted-foreground hover:bg-accent hover:text-foreground" | |
161 | + }`} | |
162 | + > | |
163 | + {children} | |
164 | + </button> | |
165 | + ); | |
166 | +} | |
167 | + | |
168 | +export default function CodeReviewPanel({ owner, repo, prNumber }: Props) { | |
169 | + const [analysis, setAnalysis] = useState<AnalysisData | null>(null); | |
170 | + const [comments, setComments] = useState<InlineComment[]>([]); | |
171 | + const [loading, setLoading] = useState(true); | |
172 | + const [analyzing, setAnalyzing] = useState(false); | |
173 | + const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set()); | |
174 | + const [activeTab, setActiveTab] = useState<"overview" | "groups" | "comments">("overview"); | |
175 | + | |
176 | + const fetchAnalysis = useCallback(async () => { | |
177 | + try { | |
178 | + const res = await fetch(`/api/repos/${owner}/${repo}/pulls/${prNumber}/analysis`); | |
179 | + const data = await res.json(); | |
180 | + if (data.data) { | |
181 | + setAnalysis(data.data.analysis); | |
182 | + setComments(data.data.inlineComments || []); | |
183 | + } | |
184 | + } catch (err) { | |
185 | + console.error("Failed to fetch analysis:", err); | |
186 | + } finally { | |
187 | + setLoading(false); | |
188 | + } | |
189 | + }, [owner, repo, prNumber]); | |
190 | + | |
191 | + const triggerAnalysis = useCallback(async () => { | |
192 | + setAnalyzing(true); | |
193 | + try { | |
194 | + const res = await fetch(`/api/repos/${owner}/${repo}/pulls/${prNumber}/analysis`, { | |
195 | + method: "POST", | |
196 | + }); | |
197 | + const data = await res.json(); | |
198 | + if (data.data?.status === "running") { | |
199 | + const pollInterval = setInterval(async () => { | |
200 | + const pollRes = await fetch(`/api/repos/${owner}/${repo}/pulls/${prNumber}/analysis`); | |
201 | + const pollData = await pollRes.json(); | |
202 | + if (pollData.data?.analysis?.status === "completed" || pollData.data?.analysis?.status === "failed") { | |
203 | + clearInterval(pollInterval); | |
204 | + await fetchAnalysis(); | |
205 | + setAnalyzing(false); | |
206 | + } | |
207 | + }, 3000); | |
208 | + } | |
209 | + } catch (err) { | |
210 | + console.error("Failed to trigger analysis:", err); | |
211 | + setAnalyzing(false); | |
212 | + } | |
213 | + }, [owner, repo, prNumber, fetchAnalysis]); | |
214 | + | |
215 | + useEffect(() => { | |
216 | + fetchAnalysis(); | |
217 | + }, [fetchAnalysis]); | |
218 | + | |
219 | + useEffect(() => { | |
220 | + if (analysis?.status === "running") { | |
221 | + const interval = setInterval(fetchAnalysis, 5000); | |
222 | + return () => clearInterval(interval); | |
223 | + } | |
224 | + }, [analysis?.status, fetchAnalysis]); | |
225 | + | |
226 | + const toggleGroup = (id: string) => { | |
227 | + setExpandedGroups(prev => { | |
228 | + const next = new Set(prev); | |
229 | + if (next.has(id)) next.delete(id); | |
230 | + else next.add(id); | |
231 | + return next; | |
232 | + }); | |
233 | + }; | |
234 | + | |
235 | + if (loading) { | |
236 | + return ( | |
237 | + <div className="border border-border bg-card rounded-lg p-6"> | |
238 | + <div className="flex items-center gap-3"> | |
239 | + <RefreshCw className="h-5 w-5 text-muted-foreground animate-spin" /> | |
240 | + <span className="text-sm text-muted-foreground">Loading analysis...</span> | |
241 | + </div> | |
242 | + </div> | |
243 | + ); | |
244 | + } | |
245 | + | |
246 | + if (!analysis) { | |
247 | + return ( | |
248 | + <div className="border border-border bg-card rounded-lg p-6"> | |
249 | + <div className="text-center"> | |
250 | + <Shield className="h-10 w-10 text-muted-foreground mx-auto mb-3" /> | |
251 | + <h3 className="text-sm font-semibold text-foreground mb-1">No Analysis Yet</h3> | |
252 | + <p className="text-xs text-muted-foreground mb-4"> | |
253 | + Run an AI-powered analysis to get code review insights. | |
254 | + </p> | |
255 | + <button | |
256 | + onClick={triggerAnalysis} | |
257 | + disabled={analyzing} | |
258 | + className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50 transition-all" | |
259 | + > | |
260 | + {analyzing ? <RefreshCw className="h-4 w-4 animate-spin" /> : <Shield className="h-4 w-4" />} | |
261 | + {analyzing ? "Analyzing..." : "Run Analysis"} | |
262 | + </button> | |
263 | + </div> | |
264 | + </div> | |
265 | + ); | |
266 | + } | |
267 | + | |
268 | + const gradeColor = getGradeColor(analysis.healthGrade); | |
269 | + const riskConfig = getSeverityConfig(analysis.blastRadius?.riskLevel || "low"); | |
270 | + const RiskIcon = riskConfig.icon; | |
271 | + | |
272 | + return ( | |
273 | + <div className="space-y-4"> | |
274 | + {/* Health Score Header */} | |
275 | + <div className="border border-border bg-card rounded-lg p-4"> | |
276 | + <div className="flex items-center justify-between mb-3"> | |
277 | + <div className="flex items-center gap-3"> | |
278 | + <div className="relative"> | |
279 | + <div className={`w-14 h-14 rounded-full border-4 border-current flex items-center justify-center ${gradeColor}`}> | |
280 | + <span className="text-lg font-bold">{analysis.healthScore || "—"}</span> | |
281 | + </div> | |
282 | + <div className="absolute -bottom-1 -right-1 px-1.5 py-0.5 rounded text-[10px] font-bold bg-card border border-border"> | |
283 | + {analysis.healthGrade || "—"} | |
284 | + </div> | |
285 | + </div> | |
286 | + <div> | |
287 | + <h3 className="text-sm font-semibold text-foreground">PR Health Score</h3> | |
288 | + <p className="text-xs text-muted-foreground"> | |
289 | + {analysis.filesAnalyzed} file(s) analyzed • +{analysis.totalAdditions}/-{analysis.totalDeletions} | |
290 | + </p> | |
291 | + </div> | |
292 | + </div> | |
293 | + <button | |
294 | + onClick={triggerAnalysis} | |
295 | + disabled={analyzing} | |
296 | + className="p-2 rounded-lg border border-border hover:bg-accent disabled:opacity-50 transition-all" | |
297 | + title="Re-run analysis" | |
298 | + > | |
299 | + <RefreshCw className={`h-4 w-4 text-muted-foreground ${analyzing ? "animate-spin" : ""}`} /> | |
300 | + </button> | |
301 | + </div> | |
302 | + | |
303 | + {/* Risk Badge */} | |
304 | + <div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${riskConfig.bg} ${riskConfig.color}`}> | |
305 | + <RiskIcon className="h-3.5 w-3.5" /> | |
306 | + {analysis.blastRadius?.riskLevel?.charAt(0).toUpperCase()}{analysis.blastRadius?.riskLevel?.slice(1)} Risk | |
307 | + </div> | |
308 | + | |
309 | + {/* Blast Radius */} | |
310 | + {analysis.blastRadius && ( | |
311 | + <div className="mt-3 grid grid-cols-3 gap-2"> | |
312 | + <div className="text-center p-2 rounded-lg bg-muted/50"> | |
313 | + <div className="text-lg font-bold text-foreground">{analysis.blastRadius.directChanges}</div> | |
314 | + <div className="text-[10px] text-muted-foreground">Files Changed</div> | |
315 | + </div> | |
316 | + <div className="text-center p-2 rounded-lg bg-muted/50"> | |
317 | + <div className="text-lg font-bold text-foreground">{analysis.blastRadius.affectedRoutes}</div> | |
318 | + <div className="text-[10px] text-muted-foreground">Routes Affected</div> | |
319 | + </div> | |
320 | + <div className="text-center p-2 rounded-lg bg-muted/50"> | |
321 | + <div className="text-lg font-bold text-foreground">{analysis.blastRadius.affectedComponents}</div> | |
322 | + <div className="text-[10px] text-muted-foreground">Components</div> | |
323 | + </div> | |
324 | + </div> | |
325 | + )} | |
326 | + | |
327 | + {analysis.blastRadius?.summary && ( | |
328 | + <p className="mt-2 text-xs text-muted-foreground">{analysis.blastRadius.summary}</p> | |
329 | + )} | |
330 | + </div> | |
331 | + | |
332 | + {/* Tab Navigation */} | |
333 | + <div className="flex gap-1 border border-border bg-card rounded-lg p-1"> | |
334 | + <TabButton active={activeTab === "overview"} onClick={() => setActiveTab("overview")}> | |
335 | + <Eye className="h-3.5 w-3.5 inline mr-1" /> | |
336 | + Overview | |
337 | + </TabButton> | |
338 | + <TabButton active={activeTab === "groups"} onClick={() => setActiveTab("groups")}> | |
339 | + <Layers className="h-3.5 w-3.5 inline mr-1" /> | |
340 | + Groups | |
341 | + </TabButton> | |
342 | + <TabButton active={activeTab === "comments"} onClick={() => setActiveTab("comments")}> | |
343 | + <MessageSquare className="h-3.5 w-3.5 inline mr-1" /> | |
344 | + Comments | |
345 | + {comments.length > 0 && ( | |
346 | + <span className="ml-1 px-1.5 py-0.5 rounded-full bg-primary/20 text-primary text-[10px]"> | |
347 | + {comments.length} | |
348 | + </span> | |
349 | + )} | |
350 | + </TabButton> | |
351 | + </div> | |
352 | + | |
353 | + {/* Overview Tab */} | |
354 | + {activeTab === "overview" && ( | |
355 | + <div className="space-y-4"> | |
356 | + {/* Architecture Impact */} | |
357 | + {analysis.architectureImpact && ( | |
358 | + <div className="border border-border bg-card rounded-lg p-4"> | |
359 | + <h4 className="text-xs font-semibold text-foreground mb-3 flex items-center gap-2"> | |
360 | + <Layers className="h-4 w-4" /> | |
361 | + Architecture Impact | |
362 | + </h4> | |
363 | + <div className="space-y-2"> | |
364 | + {analysis.architectureImpact.layers.map((layer, i) => { | |
365 | + const LayerIcon = LAYER_ICONS[layer.layer] || Layers; | |
366 | + const sevColor = layer.severity === "high" ? "bg-red-500/10 text-red-500" | |
367 | + : layer.severity === "medium" ? "bg-yellow-500/10 text-yellow-500" | |
368 | + : "bg-green-500/10 text-green-500"; | |
369 | + return ( | |
370 | + <div key={i} className="flex items-center justify-between p-2 rounded-lg bg-muted/50"> | |
371 | + <div className="flex items-center gap-2"> | |
372 | + <LayerIcon className="h-4 w-4 text-muted-foreground" /> | |
373 | + <span className="text-xs font-medium text-foreground capitalize">{layer.layer}</span> | |
374 | + </div> | |
375 | + <div className="flex items-center gap-2"> | |
376 | + <span className="text-xs text-muted-foreground">{layer.changeCount} file(s)</span> | |
377 | + <span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${sevColor}`}> | |
378 | + {layer.severity} | |
379 | + </span> | |
380 | + </div> | |
381 | + </div> | |
382 | + ); | |
383 | + })} | |
384 | + </div> | |
385 | + {analysis.architectureImpact.crossLayerChanges && ( | |
386 | + <div className="mt-3 flex items-center gap-2 px-3 py-2 rounded-lg bg-yellow-500/10 text-yellow-500 text-xs"> | |
387 | + <AlertTriangle className="h-4 w-4" /> | |
388 | + Changes span multiple architectural layers — review carefully | |
389 | + </div> | |
390 | + )} | |
391 | + {analysis.architectureImpact.securityImpact && ( | |
392 | + <div className="mt-2 flex items-center gap-2 px-3 py-2 rounded-lg bg-red-500/10 text-red-500 text-xs"> | |
393 | + <ShieldAlert className="h-4 w-4" /> | |
394 | + Security-related files modified | |
395 | + </div> | |
396 | + )} | |
397 | + </div> | |
398 | + )} | |
399 | + | |
400 | + {/* Complexity */} | |
401 | + {analysis.complexityData && ( | |
402 | + <div className="border border-border bg-card rounded-lg p-4"> | |
403 | + <h4 className="text-xs font-semibold text-foreground mb-3 flex items-center gap-2"> | |
404 | + <TrendingUp className="h-4 w-4" /> | |
405 | + Complexity Analysis | |
406 | + </h4> | |
407 | + <div className="grid grid-cols-2 gap-2"> | |
408 | + <div className="p-2 rounded-lg bg-muted/50"> | |
409 | + <div className="text-lg font-bold text-foreground">{analysis.complexityData.cyclomaticComplexity}</div> | |
410 | + <div className="text-[10px] text-muted-foreground">Cyclomatic Complexity</div> | |
411 | + </div> | |
412 | + <div className="p-2 rounded-lg bg-muted/50"> | |
413 | + <div className="text-lg font-bold text-foreground">{analysis.complexityData.nestingDepth}</div> | |
414 | + <div className="text-[10px] text-muted-foreground">Max Nesting Depth</div> | |
415 | + </div> | |
416 | + </div> | |
417 | + {analysis.complexityData.longFunctions.length > 0 && ( | |
418 | + <div className="mt-3 space-y-1"> | |
419 | + <p className="text-[10px] text-muted-foreground font-medium">Long Functions:</p> | |
420 | + {analysis.complexityData.longFunctions.map((fn, i) => ( | |
421 | + <div key={i} className="flex items-center gap-2 text-xs text-muted-foreground"> | |
422 | + <Minus className="h-3 w-3" /> | |
423 | + <span className="font-mono">{fn.name}</span> | |
424 | + <span className="text-yellow-500">({fn.lines} lines)</span> | |
425 | + </div> | |
426 | + ))} | |
427 | + </div> | |
428 | + )} | |
429 | + </div> | |
430 | + )} | |
431 | + | |
432 | + {/* AI Summary */} | |
433 | + {analysis.summaries?.overall && ( | |
434 | + <div className="border border-border bg-card rounded-lg p-4"> | |
435 | + <h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-2"> | |
436 | + <FileText className="h-4 w-4" /> | |
437 | + AI Summary | |
438 | + </h4> | |
439 | + <p className="text-sm text-foreground leading-relaxed"> | |
440 | + {analysis.summaries.overall.summary} | |
441 | + </p> | |
442 | + {analysis.summaries.overall.keyChanges?.length > 0 && ( | |
443 | + <ul className="mt-3 space-y-1"> | |
444 | + {analysis.summaries.overall.keyChanges.map((change: string, i: number) => ( | |
445 | + <li key={i} className="flex items-start gap-2 text-xs text-muted-foreground"> | |
446 | + <CheckCircle2 className="h-3.5 w-3.5 mt-0.5 flex-shrink-0 text-green-500" /> | |
447 | + {change} | |
448 | + </li> | |
449 | + ))} | |
450 | + </ul> | |
451 | + )} | |
452 | + </div> | |
453 | + )} | |
454 | + | |
455 | + {/* AI Provider Info */} | |
456 | + {analysis.provider && ( | |
457 | + <div className="flex items-center gap-2 text-[10px] text-muted-foreground px-1"> | |
458 | + <Zap className="h-3 w-3" /> | |
459 | + Analyzed by {analysis.provider}{analysis.model ? ` (${analysis.model})` : ""} | |
460 | + </div> | |
461 | + )} | |
462 | + </div> | |
463 | + )} | |
464 | + | |
465 | + {/* Groups Tab */} | |
466 | + {activeTab === "groups" && ( | |
467 | + <div className="space-y-2"> | |
468 | + {analysis.changeGroups && analysis.changeGroups.length > 0 ? ( | |
469 | + analysis.changeGroups.map(group => { | |
470 | + const isExpanded = expandedGroups.has(group.id); | |
471 | + const groupRisk = getSeverityConfig(group.riskLevel); | |
472 | + const GroupRiskIcon = groupRisk.icon; | |
473 | + const LayerIcon = LAYER_ICONS[group.layer] || Layers; | |
474 | + | |
475 | + return ( | |
476 | + <div key={group.id} className="border border-border bg-card rounded-lg overflow-hidden"> | |
477 | + <button | |
478 | + onClick={() => toggleGroup(group.id)} | |
479 | + className="w-full flex items-center justify-between p-3 hover:bg-accent/50 transition-colors" | |
480 | + > | |
481 | + <div className="flex items-center gap-3"> | |
482 | + {isExpanded ? <ChevronDown className="h-4 w-4 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 text-muted-foreground" />} | |
483 | + <LayerIcon className="h-4 w-4 text-muted-foreground" /> | |
484 | + <div className="text-left"> | |
485 | + <div className="text-sm font-medium text-foreground">{group.title}</div> | |
486 | + <div className="text-[10px] text-muted-foreground"> | |
487 | + {group.files.length} file(s) • +{group.totalAdditions}/-{group.totalDeletions} | |
488 | + </div> | |
489 | + </div> | |
490 | + </div> | |
491 | + <div className="flex items-center gap-2"> | |
492 | + <span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${groupRisk.bg} ${groupRisk.color}`}> | |
493 | + {group.riskLevel} | |
494 | + </span> | |
495 | + </div> | |
496 | + </button> | |
497 | + | |
498 | + <AnimatePresence> | |
499 | + {isExpanded && ( | |
500 | + <motion.div | |
501 | + initial={{ height: 0 }} | |
502 | + animate={{ height: "auto" }} | |
503 | + exit={{ height: 0 }} | |
504 | + className="overflow-hidden" | |
505 | + > | |
506 | + <div className="border-t border-border p-3 space-y-1"> | |
507 | + {group.files.map(file => ( | |
508 | + <div key={file} className="flex items-center gap-2 text-xs text-muted-foreground font-mono"> | |
509 | + <FileText className="h-3 w-3 flex-shrink-0" /> | |
510 | + <span className="truncate">{file}</span> | |
511 | + </div> | |
512 | + ))} | |
513 | + </div> | |
514 | + </motion.div> | |
515 | + )} | |
516 | + </AnimatePresence> | |
517 | + </div> | |
518 | + ); | |
519 | + }) | |
520 | + ) : ( | |
521 | + <div className="border border-dashed border-border rounded-lg py-8 text-center"> | |
522 | + <Layers className="h-8 w-8 text-muted-foreground mx-auto mb-2" /> | |
523 | + <p className="text-xs text-muted-foreground">No change groups available</p> | |
524 | + </div> | |
525 | + )} | |
526 | + </div> | |
527 | + )} | |
528 | + | |
529 | + {/* Comments Tab */} | |
530 | + {activeTab === "comments" && ( | |
531 | + <div className="space-y-2"> | |
532 | + {comments.length > 0 ? ( | |
533 | + comments.map(comment => { | |
534 | + const cfg = getSeverityConfig(comment.severity); | |
535 | + const CommentIcon = cfg.icon; | |
536 | + return ( | |
537 | + <div key={comment.id} className="border border-border bg-card rounded-lg p-3"> | |
538 | + <div className="flex items-start justify-between gap-2 mb-2"> | |
539 | + <div className="flex items-center gap-2"> | |
540 | + <CommentIcon className={`h-4 w-4 ${cfg.color}`} /> | |
541 | + <span className="text-sm font-medium text-foreground">{comment.title}</span> | |
542 | + </div> | |
543 | + <span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${cfg.bg} ${cfg.color}`}> | |
544 | + {comment.severity} | |
545 | + </span> | |
546 | + </div> | |
547 | + <p className="text-xs text-foreground leading-relaxed mb-2">{comment.message}</p> | |
548 | + <div className="flex items-center gap-2 text-[10px] text-muted-foreground"> | |
549 | + <span className="font-mono">{comment.filePath}:{comment.line}</span> | |
550 | + <span>•</span> | |
551 | + <span className="capitalize">{comment.type}</span> | |
552 | + {comment.confidence != null && ( | |
553 | + <> | |
554 | + <span>•</span> | |
555 | + <span>{comment.confidence}% confidence</span> | |
556 | + </> | |
557 | + )} | |
558 | + </div> | |
559 | + {comment.suggestedFix && ( | |
560 | + <div className="mt-2 p-2 rounded bg-muted/50 text-xs font-mono text-foreground overflow-x-auto"> | |
561 | + {comment.suggestedFix} | |
562 | + </div> | |
563 | + )} | |
564 | + </div> | |
565 | + ); | |
566 | + }) | |
567 | + ) : ( | |
568 | + <div className="border border-dashed border-border rounded-lg py-8 text-center"> | |
569 | + <MessageSquare className="h-8 w-8 text-muted-foreground mx-auto mb-2" /> | |
570 | + <p className="text-xs text-muted-foreground">No inline comments yet</p> | |
571 | + </div> | |
572 | + )} | |
573 | + </div> | |
574 | + )} | |
575 | + | |
576 | + {/* Error State */} | |
577 | + {analysis.status === "failed" && analysis.errorMessage && ( | |
578 | + <div className="border border-red-500/30 bg-red-500/5 rounded-lg p-4"> | |
579 | + <div className="flex items-center gap-2 text-sm text-red-500"> | |
580 | + <XCircle className="h-4 w-4" /> | |
581 | + Analysis failed: {analysis.errorMessage} | |
582 | + </div> | |
583 | + </div> | |
584 | + )} | |
585 | + | |
586 | + {/* Running State */} | |
587 | + {analysis.status === "running" && ( | |
588 | + <div className="border border-primary/30 bg-primary/5 rounded-lg p-4"> | |
589 | + <div className="flex items-center gap-2 text-sm text-primary"> | |
590 | + <RefreshCw className="h-4 w-4 animate-spin" /> | |
591 | + Analysis in progress... | |
592 | + </div> | |
593 | + </div> | |
594 | + )} | |
595 | + </div> | |
596 | + ); | |
597 | +} | |
0 | 598 | |