Download src/components/editor/DiffViewer.tsx from Leon4gr45/Velo: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/spaces/Leon4gr45/Velo/resolve/main/src/components/editor/DiffViewer.tsx
- Command line
-
hf download hf://spaces/Leon4gr45/Velo/src/components/editor/DiffViewer.tsx
-
curl -L -o DiffViewer.tsx https://huggingface.co/spaces/Leon4gr45/Velo/resolve/main/src/components/editor/DiffViewer.tsx
10.2 kB
| import { useState, useMemo } from 'react' | |
| import { ArrowLeftRight, Check, X } from 'lucide-react' | |
| import { useTranslation } from 'react-i18next' | |
| interface DiffViewerProps { | |
| /** 原始文本 */ | |
| original: string | |
| /** 修改后文本 */ | |
| modified: string | |
| /** 原始文本标签 */ | |
| originalLabel?: string | |
| /** 修改后文本标签 */ | |
| modifiedLabel?: string | |
| /** 接受修改回调 */ | |
| onAccept?: (text: string) => void | |
| /** 拒绝修改回调 */ | |
| onReject?: () => void | |
| } | |
| interface DiffLine { | |
| type: 'same' | 'add' | 'remove' | |
| content: string | |
| lineOld?: number | |
| lineNew?: number | |
| } | |
| /** | |
| * 文本对比查看器 — 并排显示原文和修改后的差异 | |
| * 类似 VS Code 的 diff 视图 | |
| */ | |
| export default function DiffViewer({ | |
| original, | |
| modified, | |
| originalLabel, | |
| modifiedLabel, | |
| onAccept, | |
| onReject, | |
| }: DiffViewerProps) { | |
| const { t } = useTranslation('editors') | |
| const resolvedOriginalLabel = originalLabel ?? t('diffViewer.original') | |
| const resolvedModifiedLabel = modifiedLabel ?? t('diffViewer.revised') | |
| const [mode, setMode] = useState<'side' | 'inline'>('side') | |
| // 计算差异 | |
| const diffLines = useMemo(() => computeDiff(original, modified), [original, modified]) | |
| // 统计 | |
| const addCount = diffLines.filter((d) => d.type === 'add').length | |
| const removeCount = diffLines.filter((d) => d.type === 'remove').length | |
| return ( | |
| <div className="flex flex-col h-full"> | |
| {/* 头部工具栏 */} | |
| <div | |
| className="no-select flex items-center justify-between px-4 h-9 flex-shrink-0" | |
| style={{ | |
| borderBottom: '1px solid var(--color-border)', | |
| backgroundColor: 'var(--color-sidebar)', | |
| }} | |
| > | |
| <div className="flex items-center gap-3"> | |
| <ArrowLeftRight size={14} style={{ color: 'var(--color-accent)' }} /> | |
| <span className="text-xs font-medium" style={{ color: 'var(--color-text)' }}> | |
| {t('diffViewer.title')} | |
| </span> | |
| <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}> | |
| <span style={{ color: 'var(--color-success)' }}>+{addCount}</span> | |
| {' '} | |
| <span style={{ color: 'var(--color-error)' }}>-{removeCount}</span> | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| {/* 视图切换 */} | |
| <button | |
| onClick={() => setMode(mode === 'side' ? 'inline' : 'side')} | |
| className="px-2 py-0.5 rounded text-xs" | |
| style={{ | |
| backgroundColor: 'var(--color-hover)', | |
| color: 'var(--color-text-secondary)', | |
| border: '1px solid var(--color-border)', | |
| }} | |
| > | |
| {mode === 'side' ? t('diffViewer.sideBySide') : t('diffViewer.inline')} | |
| </button> | |
| {/* 操作按钮 */} | |
| {onReject && ( | |
| <button | |
| onClick={onReject} | |
| className="flex items-center gap-1 px-2 py-0.5 rounded text-xs" | |
| style={{ color: 'var(--color-error)', border: '1px solid var(--color-border)' }} | |
| > | |
| <X size={12} /> {t('diffViewer.reject')} | |
| </button> | |
| )} | |
| {onAccept && ( | |
| <button | |
| onClick={() => onAccept(modified)} | |
| className="flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium" | |
| style={{ | |
| backgroundColor: 'var(--color-accent)', | |
| color: '#fff', | |
| }} | |
| > | |
| <Check size={12} /> {t('diffViewer.acceptChanges')} | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| {/* Diff 内容 */} | |
| {mode === 'side' ? ( | |
| <SideBySideView diffLines={diffLines} originalLabel={resolvedOriginalLabel} modifiedLabel={resolvedModifiedLabel} /> | |
| ) : ( | |
| <InlineView diffLines={diffLines} /> | |
| )} | |
| </div> | |
| ) | |
| } | |
| /** 并排视图 */ | |
| function SideBySideView({ diffLines, originalLabel, modifiedLabel }: { | |
| diffLines: DiffLine[]; originalLabel: string; modifiedLabel: string | |
| }) { | |
| return ( | |
| <div className="flex flex-1 overflow-hidden"> | |
| {/* 左侧:原文 */} | |
| <div className="flex-1 flex flex-col" style={{ borderRight: '1px solid var(--color-border)' }}> | |
| <div | |
| className="px-3 py-1 text-xs font-medium flex-shrink-0" | |
| style={{ | |
| backgroundColor: 'color-mix(in srgb, var(--color-error) 6%, transparent)', | |
| color: 'var(--color-error)', | |
| borderBottom: '1px solid var(--color-border)', | |
| }} | |
| > | |
| {originalLabel} | |
| </div> | |
| <div className="flex-1 overflow-y-auto font-mono text-xs leading-5"> | |
| {diffLines | |
| .filter((d) => d.type !== 'add') | |
| .map((d, i) => ( | |
| <div | |
| key={i} | |
| className="px-3 py-0.5" | |
| style={{ | |
| backgroundColor: d.type === 'remove' ? 'color-mix(in srgb, var(--color-error) 8%, transparent)' : 'transparent', | |
| color: d.type === 'remove' ? 'var(--color-error)' : 'var(--color-text)', | |
| }} | |
| > | |
| <span className="inline-block w-8 text-right mr-2 opacity-30">{d.lineOld ?? ''}</span> | |
| {d.type === 'remove' && <span className="mr-1">-</span>} | |
| {d.content} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* 右侧:修改后 */} | |
| <div className="flex-1 flex flex-col"> | |
| <div | |
| className="px-3 py-1 text-xs font-medium flex-shrink-0" | |
| style={{ | |
| backgroundColor: 'color-mix(in srgb, var(--color-success) 6%, transparent)', | |
| color: 'var(--color-success)', | |
| borderBottom: '1px solid var(--color-border)', | |
| }} | |
| > | |
| {modifiedLabel} | |
| </div> | |
| <div className="flex-1 overflow-y-auto font-mono text-xs leading-5"> | |
| {diffLines | |
| .filter((d) => d.type !== 'remove') | |
| .map((d, i) => ( | |
| <div | |
| key={i} | |
| className="px-3 py-0.5" | |
| style={{ | |
| backgroundColor: d.type === 'add' ? 'color-mix(in srgb, var(--color-success) 8%, transparent)' : 'transparent', | |
| color: d.type === 'add' ? 'var(--color-success)' : 'var(--color-text)', | |
| }} | |
| > | |
| <span className="inline-block w-8 text-right mr-2 opacity-30">{d.lineNew ?? ''}</span> | |
| {d.type === 'add' && <span className="mr-1">+</span>} | |
| {d.content} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| /** 内联视图 */ | |
| function InlineView({ diffLines }: { diffLines: DiffLine[] }) { | |
| return ( | |
| <div className="flex-1 overflow-y-auto font-mono text-xs leading-5 p-0"> | |
| {diffLines.map((d, i) => ( | |
| <div | |
| key={i} | |
| className="px-4 py-0.5" | |
| style={{ | |
| backgroundColor: | |
| d.type === 'add' ? 'color-mix(in srgb, var(--color-success) 8%, transparent)' | |
| : d.type === 'remove' ? 'color-mix(in srgb, var(--color-error) 8%, transparent)' | |
| : 'transparent', | |
| color: | |
| d.type === 'add' ? 'var(--color-success)' | |
| : d.type === 'remove' ? 'var(--color-error)' | |
| : 'var(--color-text)', | |
| }} | |
| > | |
| <span className="inline-block w-8 text-right mr-1 opacity-30">{d.lineOld ?? ''}</span> | |
| <span className="inline-block w-8 text-right mr-2 opacity-30">{d.lineNew ?? ''}</span> | |
| <span className="mr-1">{d.type === 'add' ? '+' : d.type === 'remove' ? '-' : ' '}</span> | |
| {d.content} | |
| </div> | |
| ))} | |
| </div> | |
| ) | |
| } | |
| /** | |
| * 简单行级 diff 算法(LCS-based) | |
| * 生产环境建议替换为 diff-match-patch 或类似库 | |
| */ | |
| function computeDiff(original: string, modified: string): DiffLine[] { | |
| const oldLines = original.split('\n') | |
| const newLines = modified.split('\n') | |
| // 简化实现:逐行对比,使用 LCS 找出公共子序列 | |
| const lcs = longestCommonSubsequence(oldLines, newLines) | |
| const result: DiffLine[] = [] | |
| let oldIdx = 0 | |
| let newIdx = 0 | |
| let oldLineNum = 1 | |
| let newLineNum = 1 | |
| for (const common of lcs) { | |
| // 输出 old 中在 common 之前的删除行 | |
| while (oldIdx < oldLines.length && oldLines[oldIdx] !== common) { | |
| result.push({ type: 'remove', content: oldLines[oldIdx], lineOld: oldLineNum++ }) | |
| oldIdx++ | |
| } | |
| // 输出 new 中在 common 之前的新增行 | |
| while (newIdx < newLines.length && newLines[newIdx] !== common) { | |
| result.push({ type: 'add', content: newLines[newIdx], lineNew: newLineNum++ }) | |
| newIdx++ | |
| } | |
| // 公共行 | |
| result.push({ type: 'same', content: common, lineOld: oldLineNum++, lineNew: newLineNum++ }) | |
| oldIdx++ | |
| newIdx++ | |
| } | |
| // 处理尾部 | |
| while (oldIdx < oldLines.length) { | |
| result.push({ type: 'remove', content: oldLines[oldIdx++], lineOld: oldLineNum++ }) | |
| } | |
| while (newIdx < newLines.length) { | |
| result.push({ type: 'add', content: newLines[newIdx++], lineNew: newLineNum++ }) | |
| } | |
| return result | |
| } | |
| /** 最长公共子序列(行级) */ | |
| function longestCommonSubsequence(a: string[], b: string[]): string[] { | |
| const m = a.length | |
| const n = b.length | |
| // 优化:对于大文本只取前后各 500 行 | |
| if (m > 1000 || n > 1000) { | |
| return simpleFallback(a, b) | |
| } | |
| const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)) | |
| for (let i = 1; i <= m; i++) { | |
| for (let j = 1; j <= n; j++) { | |
| if (a[i - 1] === b[j - 1]) { | |
| dp[i][j] = dp[i - 1][j - 1] + 1 | |
| } else { | |
| dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) | |
| } | |
| } | |
| } | |
| // 回溯 | |
| const result: string[] = [] | |
| let i = m, j = n | |
| while (i > 0 && j > 0) { | |
| if (a[i - 1] === b[j - 1]) { | |
| result.unshift(a[i - 1]) | |
| i-- | |
| j-- | |
| } else if (dp[i - 1][j] > dp[i][j - 1]) { | |
| i-- | |
| } else { | |
| j-- | |
| } | |
| } | |
| return result | |
| } | |
| /** 大文本降级:只保留完全相同的行 */ | |
| function simpleFallback(a: string[], b: string[]): string[] { | |
| const bSet = new Set(b) | |
| return a.filter((line) => bSet.has(line)) | |
| } | |