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 ( +
+ {t('skillDetail.binaryFileNotice')} +
+ ) + } + + const isLoading = sourceLoading || targetLoading + const hasError = sourceError || targetError + + if (isLoading) { + return ( +
+
+
+
+
+ ) + } + + if (hasError) { + return ( +
+
+ + {t('skillDetail.diffLoadError')} +
+ +
+ ) + } + + const oldValue = changeType === 'added' ? '' : (sourceContent || '') + const newValue = changeType === 'removed' ? '' : (targetContent || '') + + return ( +
+ {isLargeFile(fileSize) && ( +
+ {t('skillDetail.largeFileWarning')} +
+ )} +
+
+ + +
+
+
+ +
+
+ ) +} From 7622c081d3ae6cfb38232f49157c1723d423d443 Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Thu, 23 Apr 2026 11:17:21 +0800 Subject: [PATCH 2/4] feat(ui): render interactive file diff list in version compare dialog --- web/src/pages/skill-detail.tsx | 135 +++++++++++++++++++++++++++++++-- 1 file changed, 128 insertions(+), 7 deletions(-) diff --git a/web/src/pages/skill-detail.tsx b/web/src/pages/skill-detail.tsx index 19f649835..572326a6d 100644 --- a/web/src/pages/skill-detail.tsx +++ b/web/src/pages/skill-detail.tsx @@ -2,7 +2,8 @@ import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useParams, useNavigate, useRouterState, useSearch } from '@tanstack/react-router' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { ArrowLeft, ArrowUpCircle, ChevronDown, ChevronUp, Clock, Folder, Globe, Lock, RefreshCw, ShieldCheck, Terminal, User, Users } from 'lucide-react' +import { ArrowLeft, ArrowUpCircle, ChevronDown, ChevronUp, Clock, FilePlus, FileMinus, FileEdit, Folder, Globe, Lock, RefreshCw, ShieldCheck, Terminal, User, Users } from 'lucide-react' +import { FileDiffViewer } from '@/features/skill/file-diff-viewer' import { MarkdownRenderer } from '@/features/skill/markdown-renderer' import { FileTree } from '@/features/skill/file-tree' import { FilePreviewDialog } from '@/features/skill/file-preview-dialog' @@ -96,6 +97,64 @@ function getPromotionConflictKey(error: ApiError): 'promotion.duplicate_pending' return null } +function FileChangeItem({ + namespace, + slug, + sourceVersion, + targetVersion, + filePath, + fileSize, + changeType, +}: { + namespace: string + slug: string + sourceVersion: string | null + targetVersion: string | null + filePath: string + fileSize: number + changeType: 'added' | 'removed' | 'modified' +}) { + const [isOpen, setIsOpen] = useState(false) + const { t } = useTranslation() + + const Icon = changeType === 'added' ? FilePlus : changeType === 'removed' ? FileMinus : FileEdit + const iconColor = changeType === 'added' ? 'text-emerald-500' : changeType === 'removed' ? 'text-red-500' : 'text-amber-500' + + return ( +
+ + {isOpen && ( +
+ +
+ )} +
+ ) +} + export function SkillDetailPage() { const { t, i18n } = useTranslation() const navigate = useNavigate() @@ -1520,7 +1579,7 @@ export function SkillDetailPage() { } }} > - + {t('skillDetail.compareDialogTitle')} @@ -1529,7 +1588,7 @@ export function SkillDetailPage() { : ''} -
+
{t('skillDetail.compareSourceLabel')}
@@ -1566,22 +1625,78 @@ export function SkillDetailPage() {
-
+
{t('skillDetail.fileChanges')}
+ + {/* File stats summary */}
{t('skillDetail.filesAdded')}
-
{fileDiffSummary.added.length}
+
{fileDiffSummary.added.length}
{t('skillDetail.filesRemoved')}
-
{fileDiffSummary.removed.length}
+
{fileDiffSummary.removed.length}
{t('skillDetail.filesChanged')}
-
{fileDiffSummary.changed.length}
+
{fileDiffSummary.changed.length}
+ + {/* Detailed file list */} +
+ {/* Modified files */} + {fileDiffSummary.changed.map(path => { + const file = diffSourceFiles?.find(f => f.filePath === path) || diffCompareFiles?.find(f => f.filePath === path) + return ( + + ) + })} + + {/* Added files */} + {fileDiffSummary.added.map(path => { + const file = diffCompareFiles?.find(f => f.filePath === path) + return ( + + ) + })} + + {/* Removed files */} + {fileDiffSummary.removed.map(path => { + const file = diffSourceFiles?.find(f => f.filePath === path) + return ( + + ) + })} +
@@ -1595,6 +1710,12 @@ export function SkillDetailPage() { {t('dialog.close')} +