From e2da54dfb3cccdffc32eae24cae12d67c499a249 Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Thu, 23 Apr 2026 11:10:01 +0800 Subject: [PATCH 1/4] feat(ui): create FileDiffViewer component for version comparison --- web/src/features/skill/file-diff-viewer.tsx | 157 ++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 web/src/features/skill/file-diff-viewer.tsx diff --git a/web/src/features/skill/file-diff-viewer.tsx b/web/src/features/skill/file-diff-viewer.tsx new file mode 100644 index 000000000..086e8d11a --- /dev/null +++ b/web/src/features/skill/file-diff-viewer.tsx @@ -0,0 +1,157 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import ReactDiffViewer from 'react-diff-viewer-continued' +import { AlertCircle, RefreshCw } from 'lucide-react' +import { useSkillFile } from '@/shared/hooks/use-skill-queries' +import { Button } from '@/shared/ui/button' + +const BINARY_EXTENSIONS = new Set([ + 'png', 'jpg', 'jpeg', 'gif', 'ico', 'woff', 'woff2', 'ttf', 'eot', + 'zip', 'tar', 'gz', 'jar', 'war', 'class', 'so', 'dll', 'exe', 'pdf' +]) + +function isBinaryFile(filePath: string): boolean { + const ext = filePath.split('.').pop()?.toLowerCase() || '' + return BINARY_EXTENSIONS.has(ext) +} + +function isLargeFile(sizeBytes: number): boolean { + return sizeBytes > 1024 * 1024 // 1MB +} + +interface FileDiffViewerProps { + namespace: string + slug: string + sourceVersion: string | null + targetVersion: string | null + filePath: string + fileSize: number + changeType: 'added' | 'removed' | 'modified' + isExpanded: boolean +} + +export function FileDiffViewer({ + namespace, + slug, + sourceVersion, + targetVersion, + filePath, + fileSize, + changeType, + isExpanded, +}: FileDiffViewerProps) { + const { t } = useTranslation() + const [splitView, setSplitView] = useState(false) + + // Only fetch source version if it's not a newly added file + const { + data: sourceContent, + isLoading: sourceLoading, + error: sourceError, + refetch: refetchSource + } = useSkillFile( + namespace, + slug, + sourceVersion ?? undefined, + changeType !== 'added' ? filePath : null, + isExpanded + ) + + // Only fetch target version if it's not a removed file + const { + data: targetContent, + isLoading: targetLoading, + error: targetError, + refetch: refetchTarget + } = useSkillFile( + namespace, + slug, + targetVersion ?? undefined, + changeType !== 'removed' ? filePath : null, + isExpanded + ) + + if (!isExpanded) return null + + if (isBinaryFile(filePath)) { + return ( +