// ======================================== // ReviewSessionPage Component // ======================================== // Review session detail page with findings display, multi-select, dimension tabs, and fix progress carousel import * as React from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useIntl } from 'react-intl'; import { ArrowLeft, Search, CheckCircle, XCircle, AlertTriangle, Info, FileText, Download, ChevronRight, ChevronLeft as ChevronLeftIcon, ChevronRight as ChevronRightIcon, } from 'lucide-react'; import { useReviewSession } from '@/hooks/useReviewSession'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; import { Card, CardContent } from '@/components/ui/Card'; type SeverityFilter = 'all' | 'critical' | 'high' | 'medium' | 'low'; type SortField = 'severity' | 'dimension' | 'file'; type SortOrder = 'asc' | 'desc'; interface FindingWithSelection { id: string; title: string; description?: string; severity: 'critical' | 'high' | 'medium' | 'low'; dimension: string; category?: string; file?: string; line?: string; code_context?: string; recommendations?: string[]; root_cause?: string; impact?: string; } // Fix Progress Types interface FixStage { stage: number; status: 'completed' | 'in-progress' | 'pending'; groups: string[]; } interface FixProgressData { fix_session_id: string; phase: 'planning' | 'execution' | 'completion'; total_findings: number; fixed_count: number; failed_count: number; in_progress_count: number; pending_count: number; percent_complete: number; current_stage: number; total_stages: number; stages: FixStage[]; active_agents: Array<{ agent_id: string; group_id: string; current_finding: { finding_title: string } | null; }>; } /** * Fix Progress Carousel Component * Displays fix progress with polling and carousel navigation */ function FixProgressCarousel({ sessionId }: { sessionId: string }) { const { formatMessage } = useIntl(); const [fixProgressData, setFixProgressData] = React.useState(null); const [currentSlide, setCurrentSlide] = React.useState(0); const [isLoading, setIsLoading] = React.useState(false); // Sequential polling with AbortController โ€” no concurrent requests possible React.useEffect(() => { const abortController = new AbortController(); let timeoutId: ReturnType | null = null; let stopped = false; let errorCount = 0; const poll = async () => { if (stopped) return; setIsLoading(true); try { const response = await fetch( `/api/fix-progress?sessionId=${encodeURIComponent(sessionId)}`, { signal: abortController.signal } ); if (!response.ok) { errorCount += 1; if (response.status === 404 || errorCount >= 3) { stopped = true; setFixProgressData(null); return; } } else { errorCount = 0; const data = await response.json(); setFixProgressData(data); if (data?.phase === 'completion') { stopped = true; return; } } } catch { if (abortController.signal.aborted) return; errorCount += 1; if (errorCount >= 3) { stopped = true; return; } } finally { if (!abortController.signal.aborted) { setIsLoading(false); } } // Schedule next poll only after current request completes if (!stopped) { timeoutId = setTimeout(poll, 5000); } }; poll(); return () => { stopped = true; abortController.abort(); if (timeoutId) clearTimeout(timeoutId); }; }, [sessionId]); // Navigate carousel const navigateSlide = (direction: 'prev' | 'next' | number) => { if (!fixProgressData) return; const totalSlides = fixProgressData.active_agents.length > 0 ? 3 : 2; if (typeof direction === 'number') { setCurrentSlide(direction); } else if (direction === 'next') { setCurrentSlide((prev) => (prev + 1) % totalSlides); } else if (direction === 'prev') { setCurrentSlide((prev) => (prev - 1 + totalSlides) % totalSlides); } }; if (isLoading && !fixProgressData) { return (
); } if (!fixProgressData) { return null; } const { phase, total_findings, fixed_count, failed_count, in_progress_count, pending_count, percent_complete, current_stage, total_stages, stages, active_agents } = fixProgressData; const phaseIcon = phase === 'planning' ? '๐Ÿ“' : phase === 'execution' ? 'โšก' : 'โœ…'; const totalSlides = active_agents.length > 0 ? 3 : 2; return ( {/* Header */}
๐Ÿ”ง {formatMessage({ id: 'reviewSession.fixProgress.title' })}
{/* Stage Dots */}
{stages.map((stage, i) => (
))}
{/* Carousel */}
{/* Slide 1: Overview */}
{phaseIcon} {formatMessage({ id: `reviewSession.fixProgress.phase.${phase}` })} {fixProgressData.fix_session_id}
{formatMessage({ id: 'reviewSession.fixProgress.complete' }, { percent: percent_complete.toFixed(0) })} ยท {formatMessage({ id: 'reviewSession.fixProgress.stage' })} {current_stage}/{total_stages}
{/* Slide 2: Stats */}
{total_findings}
{formatMessage({ id: 'reviewSession.fixProgress.stats.total' })}
{fixed_count}
{formatMessage({ id: 'reviewSession.fixProgress.stats.fixed' })}
{failed_count}
{formatMessage({ id: 'reviewSession.fixProgress.stats.failed' })}
{pending_count + in_progress_count}
{formatMessage({ id: 'reviewSession.fixProgress.stats.pending' })}
{/* Slide 3: Active Agents (if any) */} {active_agents.length > 0 && (
{active_agents.length} {active_agents.length === 1 ? formatMessage({ id: 'reviewSession.fixProgress.activeAgents' }) : formatMessage({ id: 'reviewSession.fixProgress.activeAgentsPlural' })}
{active_agents.slice(0, 2).map((agent, i) => (
๐Ÿค– {agent.current_finding?.finding_title || formatMessage({ id: 'reviewSession.fixProgress.working' })}
))}
)}
{/* Carousel Navigation */} {totalSlides > 1 && (
{Array.from({ length: totalSlides }).map((_, i) => (
)} ); } /** * ReviewSessionPage component - Display review session findings */ export function ReviewSessionPage() { const { sessionId } = useParams<{ sessionId: string }>(); const navigate = useNavigate(); const { formatMessage } = useIntl(); const { reviewSession, flattenedFindings, severityCounts, isLoading, error, refetch, } = useReviewSession(sessionId); const [severityFilter, setSeverityFilter] = React.useState>( new Set(['critical', 'high', 'medium', 'low']) ); const [dimensionFilter, setDimensionFilter] = React.useState('all'); const [searchQuery, setSearchQuery] = React.useState(''); const [sortField, setSortField] = React.useState('severity'); const [sortOrder, setSortOrder] = React.useState('desc'); const [selectedFindings, setSelectedFindings] = React.useState>(new Set()); const [selectedFindingId, setSelectedFindingId] = React.useState(null); const handleBack = () => { navigate('/sessions'); }; const toggleSeverity = (severity: SeverityFilter) => { setSeverityFilter(prev => { const next = new Set(prev); if (next.has(severity)) { next.delete(severity); } else { next.add(severity); } return next; }); }; const resetFilters = () => { setSeverityFilter(new Set(['critical', 'high', 'medium', 'low'])); setDimensionFilter('all'); setSearchQuery(''); }; const toggleSelectFinding = (findingId: string) => { setSelectedFindings(prev => { const next = new Set(prev); if (next.has(findingId)) { next.delete(findingId); } else { next.add(findingId); } return next; }); }; const selectAllFindings = () => { const validIds = filteredFindings.map(f => f.id).filter((id): id is string => id !== undefined); setSelectedFindings(new Set(validIds)); }; const selectVisibleFindings = () => { const validIds = filteredFindings.map(f => f.id).filter((id): id is string => id !== undefined); setSelectedFindings(new Set(validIds)); }; const selectBySeverity = (severity: FindingWithSelection['severity']) => { const severityIds = flattenedFindings .filter(f => f.severity === severity && f.id !== undefined) .map(f => f.id!); setSelectedFindings(prev => { const next = new Set(prev); severityIds.forEach(id => next.add(id)); return next; }); }; const clearSelection = () => { setSelectedFindings(new Set()); }; const handleFindingClick = (findingId: string) => { setSelectedFindingId(findingId); }; const exportSelectedAsJson = () => { const selected = flattenedFindings.filter(f => f.id !== undefined && selectedFindings.has(f.id)); if (selected.length === 0) return; const exportData = { session_id: sessionId, findings: selected.map(f => ({ id: f.id, title: f.title, description: f.description, severity: f.severity, dimension: f.dimension, file: f.file, line: f.line, recommendations: f.recommendations, })), }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `review-${sessionId}-fix.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; // Severity order for sorting const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 }; // Calculate dimension counts const dimensionCounts = React.useMemo(() => { const counts: Record = { all: flattenedFindings.length }; flattenedFindings.forEach(f => { counts[f.dimension] = (counts[f.dimension] || 0) + 1; }); return counts; }, [flattenedFindings]); // Filter and sort findings const filteredFindings = React.useMemo(() => { let filtered = flattenedFindings; // Apply dimension filter if (dimensionFilter !== 'all') { filtered = filtered.filter(f => f.dimension === dimensionFilter); } // Apply severity filter if (severityFilter.size > 0) { filtered = filtered.filter(f => severityFilter.has(f.severity)); } // Apply search filter if (searchQuery) { const query = searchQuery.toLowerCase(); filtered = filtered.filter(f => f.title.toLowerCase().includes(query) || f.description?.toLowerCase().includes(query) || f.file?.toLowerCase().includes(query) || f.dimension.toLowerCase().includes(query) ); } // Apply sorting filtered = [...filtered].sort((a, b) => { let comparison = 0; switch (sortField) { case 'severity': comparison = severityOrder[a.severity] - severityOrder[b.severity]; break; case 'dimension': comparison = a.dimension.localeCompare(b.dimension); break; case 'file': comparison = (a.file || '').localeCompare(b.file || ''); break; } return sortOrder === 'asc' ? comparison : -comparison; }); return filtered; }, [flattenedFindings, severityFilter, dimensionFilter, searchQuery, sortField, sortOrder]); // Get severity badge props const getSeverityBadge = (severity: FindingWithSelection['severity']) => { switch (severity) { case 'critical': return { variant: 'destructive' as const, icon: XCircle, label: formatMessage({ id: 'reviewSession.severity.critical' }) }; case 'high': return { variant: 'warning' as const, icon: AlertTriangle, label: formatMessage({ id: 'reviewSession.severity.high' }) }; case 'medium': return { variant: 'info' as const, icon: Info, label: formatMessage({ id: 'reviewSession.severity.medium' }) }; case 'low': return { variant: 'secondary' as const, icon: CheckCircle, label: formatMessage({ id: 'reviewSession.severity.low' }) }; } }; // Loading state if (isLoading) { return (
); } // Error state if (error) { return (

{formatMessage({ id: 'common.errors.loadFailed' })}

{error.message}

); } // Session not found if (!reviewSession) { return (

{formatMessage({ id: 'reviewSession.notFound.title' })}

{formatMessage({ id: 'reviewSession.notFound.message' })}

); } const dimensions = reviewSession.reviewDimensions || []; const totalFindings = flattenedFindings.length; // Determine session status (ACTIVE or ARCHIVED) const isActive = reviewSession._isActive !== false; const sessionStatus = isActive ? 'ACTIVE' : 'ARCHIVED'; const phase = reviewSession.phase || 'in-progress'; return (
{/* Header */}

๐Ÿ” {reviewSession.session_id}

Review {sessionStatus}
{/* Review Progress Section */} {/* Review Progress Header */}
๐Ÿ“Š {formatMessage({ id: 'reviewSession.progress.title' })}
{phase.toUpperCase()}
{/* Summary Cards Grid */}
๐Ÿ“Š
{totalFindings}
{formatMessage({ id: 'reviewSession.progress.totalFindings' })}
๐Ÿ”ด
{severityCounts.critical}
{formatMessage({ id: 'reviewSession.progress.critical' })}
๐ŸŸ 
{severityCounts.high}
{formatMessage({ id: 'reviewSession.progress.high' })}
๐ŸŽฏ
{dimensions.length}
{formatMessage({ id: 'reviewSession.stats.dimensions' })}
{/* Fix Progress Carousel */} {sessionId && } {/* Unified Filter Card with Dimension Tabs */} {/* Top Bar: Search + Sort + Reset */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" />
{/* Middle Row: Dimension Tabs + Severity Filters */}
{/* Dimension Tabs - Horizontal Scrollable */}
{formatMessage({ id: 'reviewSession.filters.dimension' })}
{dimensions.map(dim => ( ))}
{/* Severity Filters - Compact Pills */}
{formatMessage({ id: 'reviewSession.filters.severity' })}
{(['critical', 'high', 'medium', 'low'] as const).map(severity => { const isEnabled = severityFilter.has(severity); const colors = { critical: isEnabled ? 'bg-red-500 text-white border-red-500' : 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800', high: isEnabled ? 'bg-orange-500 text-white border-orange-500' : 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800', medium: isEnabled ? 'bg-blue-500 text-white border-blue-500' : 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800', low: isEnabled ? 'bg-gray-500 text-white border-gray-500' : 'bg-gray-50 dark:bg-gray-900/20 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800', }; return ( ); })}
{/* Bottom Bar: Selection Actions + Export */}
{selectedFindings.size > 0 ? formatMessage({ id: 'reviewSession.selection.countSelected' }, { count: selectedFindings.size }) : formatMessage({ id: 'reviewSession.selection.total' }, { count: filteredFindings.length }) } {selectedFindings.size > 0 && ( <> )} {selectedFindings.size === 0 && ( )}
{/* Split Panel: Findings List + Preview */} {filteredFindings.length === 0 ? (

{formatMessage({ id: 'reviewSession.empty.title' })}

{formatMessage({ id: 'reviewSession.empty.message' })}

) : (
{/* Left Panel: Findings List */}
{formatMessage({ id: 'reviewSession.findingsList.count' }, { count: filteredFindings.length })}
{filteredFindings.filter(f => f.id !== undefined).map(finding => { const findingId = finding.id!; const isSelected = selectedFindings.has(findingId); const isPreviewing = selectedFindingId === findingId; const badge = getSeverityBadge(finding.severity); const BadgeIcon = badge.icon; return (
handleFindingClick(findingId)} >
{/* Checkbox */} { e.stopPropagation(); toggleSelectFinding(findingId); }} className="mt-0.5 flex-shrink-0" /> {/* Compact Finding Content */}
{badge.label} {finding.dimension}

{finding.title}

{finding.file && (

{finding.file}:{finding.line || '?'}

)}
); })}
{/* Right Panel: Enhanced Preview */} {!selectedFindingId ? ( // Enhanced Empty State

{formatMessage({ id: 'reviewSession.preview.emptyTitle' })}

{formatMessage({ id: 'reviewSession.preview.empty' })}

{formatMessage({ id: 'reviewSession.preview.emptyTipSeverity' })}
๐Ÿ“ {formatMessage({ id: 'reviewSession.preview.emptyTipFile' })}
) : ( // Preview Content (() => { const finding = flattenedFindings.find(f => f.id === selectedFindingId); if (!finding) return null; const badge = getSeverityBadge(finding.severity); const BadgeIcon = badge.icon; const isSelected = selectedFindings.has(selectedFindingId); // Find adjacent findings for navigation const findingIndex = filteredFindings.findIndex(f => f.id === selectedFindingId); const prevFinding = findingIndex > 0 ? filteredFindings[findingIndex - 1] : null; const nextFinding = findingIndex < filteredFindings.length - 1 ? filteredFindings[findingIndex + 1] : null; return (
{/* Sticky Header */}
{/* Navigation + Badges Row */}
{/* Navigation Buttons */}
{findingIndex + 1} / {filteredFindings.length}
{/* Badges */}
{badge.label} {finding.dimension}
{/* Select Button */}
{/* Title */}

{finding.title}

{/* Quick Info Bar */}
{finding.file && (
๐Ÿ“ {finding.file}:{finding.line || '?'}
)}
{/* Scrollable Content */}
{/* Description */} {finding.description && (
๐Ÿ“ {formatMessage({ id: 'reviewSession.preview.description' })}

{finding.description}

)} {/* Code Context */} {finding.code_context && (
๐Ÿ’ป {formatMessage({ id: 'reviewSession.preview.codeContext' })}
                              {finding.code_context}
                            
)} {/* Root Cause */} {finding.root_cause && (
๐ŸŽฏ {formatMessage({ id: 'reviewSession.preview.rootCause' })}

{finding.root_cause}

)} {/* Impact */} {finding.impact && (
โš ๏ธ {formatMessage({ id: 'reviewSession.preview.impact' })}

{finding.impact}

)} {/* Recommendations */} {finding.recommendations && finding.recommendations.length > 0 && (
โœ… {formatMessage({ id: 'reviewSession.preview.recommendations' })}
    {finding.recommendations.map((rec, idx) => (
  • โœ“ {rec}
  • ))}
)}
); })() )}
)}
); } export default ReviewSessionPage;