// components/diff-viewer/UnifiedView.tsx import React, { useState, useMemo } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { FileText, Plus, Minus, Copy, Download, ChevronDown, ChevronRight } from 'lucide-react'; import { DiffFile, DiffPreferences } from './DiffViewerContext'; interface UnifiedViewProps { files: DiffFile[]; selectedFile: DiffFile | null; onFileSelect: (filePath: string) => void; theme: string; preferences: DiffPreferences; } interface DiffLine { type: 'added' | 'removed' | 'unchanged' | 'context'; content: string; lineNumber?: number; } const UnifiedView: React.FC = ({ files, selectedFile, onFileSelect, theme, preferences }) => { const [expandedHunks, setExpandedHunks] = useState>(new Set()); // Parse diff content into structured format const parseDiffContent = (diffContent: string): DiffLine[] => { if (!diffContent) return []; const lines = diffContent.split('\n'); const diffLines: DiffLine[] = []; let lineNumber = 0; for (const line of lines) { if (line.startsWith('@@')) { // Parse hunk header const match = line.match(/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/); if (match) { lineNumber = parseInt(match[3]) - 1; } diffLines.push({ type: 'context', content: line }); } else if (line.startsWith('+')) { lineNumber++; diffLines.push({ type: 'added', content: line.substring(1), lineNumber }); } else if (line.startsWith('-')) { diffLines.push({ type: 'removed', content: line.substring(1), lineNumber: undefined }); } else { lineNumber++; diffLines.push({ type: 'unchanged', content: line.substring(1), lineNumber }); } } return diffLines; }; // Group diff lines into hunks const groupIntoHunks = (diffLines: DiffLine[]) => { const hunks: { header: string; lines: DiffLine[] }[] = []; let currentHunk: { header: string; lines: DiffLine[] } | null = null; for (const line of diffLines) { if (line.type === 'context' && line.content.startsWith('@@')) { if (currentHunk) { hunks.push(currentHunk); } currentHunk = { header: line.content, lines: [] }; } else if (currentHunk) { currentHunk.lines.push(line); } } if (currentHunk) { hunks.push(currentHunk); } return hunks; }; const diffLines = useMemo(() => { if (!selectedFile?.diff_content) return []; return parseDiffContent(selectedFile.diff_content); }, [selectedFile]); const hunks = useMemo(() => { return groupIntoHunks(diffLines); }, [diffLines]); const toggleHunk = (hunkIndex: number) => { const hunkId = `${selectedFile?.file_path}-${hunkIndex}`; setExpandedHunks(prev => { const newSet = new Set(prev); if (newSet.has(hunkId)) { newSet.delete(hunkId); } else { newSet.add(hunkId); } return newSet; }); }; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); }; const downloadDiff = () => { if (!selectedFile?.diff_content) return; const blob = new Blob([selectedFile.diff_content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${selectedFile.file_path}.diff`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const getLineClass = (type: DiffLine['type']) => { switch (type) { case 'added': return 'bg-green-50 dark:bg-green-900/20 border-l-4 border-green-500'; case 'removed': return 'bg-red-50 dark:bg-red-900/20 border-l-4 border-red-500'; case 'unchanged': return 'bg-gray-50 dark:bg-gray-800/50'; case 'context': return 'bg-blue-50 dark:bg-blue-900/20 font-mono text-sm'; default: return ''; } }; const getLineIcon = (type: DiffLine['type']) => { switch (type) { case 'added': return ; case 'removed': return ; default: return null; } }; const getLinePrefix = (type: DiffLine['type']) => { switch (type) { case 'added': return '+'; case 'removed': return '-'; case 'unchanged': return ' '; case 'context': return '@'; default: return ' '; } }; return (
{/* File tabs */}
{files.map((file) => ( {file.file_path.split('/').pop()} {file.change_type} ))}
{/* File info and controls */} {selectedFile && (

{selectedFile.file_path}

{selectedFile.change_type} {selectedFile.diff_size_bytes && ( • {(selectedFile.diff_size_bytes / 1024).toFixed(1)} KB )}
)} {/* Diff content */}
{hunks.map((hunk, hunkIndex) => { const hunkId = `${selectedFile?.file_path}-${hunkIndex}`; const isExpanded = expandedHunks.has(hunkId); return (
toggleHunk(hunkIndex)} >
{hunk.header}
{isExpanded && (
{hunk.lines.map((line, lineIndex) => (
{line.lineNumber || ''}
{getLineIcon(line.type)}
{getLinePrefix(line.type)}
{line.content}
))}
)}
); })}
); }; export default UnifiedView;