From f5cb265667b991d59002fe6ec0ac751ef1be2930 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Thu, 5 Feb 2026 20:12:07 +0800 Subject: [PATCH] feat(annotation): implement file version management for annotation feature Add support for detecting new file versions and switching to them: Backend Changes: - Add file_version column to AnnotationResult model - Create Alembic migration for database schema update - Implement check_file_version() method to compare annotation and file versions - Implement use_new_version() method to clear annotations and update version - Update upsert_annotation() to record file version when saving - Add new API endpoints: GET /version and POST /use-new-version - Add FileVersionCheckResponse and UseNewVersionResponse schemas Frontend Changes: - Add checkFileVersionUsingGet and useNewVersionUsingPost API calls - Add version warning banner showing current vs latest file version - Add 'Use New Version' button with confirmation dialog - Clear version info state when switching files to avoid stale warnings Bug Fixes: - Fix previousFileVersion returning updated value (save before update) - Handle null file_version for historical data compatibility - Fix segmented annotation clearing (preserve structure, clear results) - Fix files without annotations incorrectly showing new version warnings - Preserve total_segments when clearing segmented annotations Files Modified: - frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx - frontend/src/pages/DataAnnotation/annotation.api.ts - runtime/datamate-python/app/db/models/annotation_management.py - runtime/datamate-python/app/module/annotation/interface/editor.py - runtime/datamate-python/app/module/annotation/schema/editor.py - runtime/datamate-python/app/module/annotation/service/editor.py New Files: - runtime/datamate-python/alembic.ini - runtime/datamate-python/alembic/env.py - runtime/datamate-python/alembic/script.py.mako - runtime/datamate-python/alembic/versions/20250205_0001_add_file_version.py --- .../Annotate/LabelStudioTextEditor.tsx | 102 +++++ .../pages/DataAnnotation/annotation.api.ts | 29 +- runtime/datamate-python/alembic.ini | 40 ++ runtime/datamate-python/alembic/env.py | 54 +++ .../datamate-python/alembic/script.py.mako | 24 ++ .../20250205_0001_add_file_version.py | 30 ++ .../app/db/models/annotation_management.py | 269 ++++++++---- .../app/module/annotation/interface/editor.py | 43 +- .../app/module/annotation/schema/editor.py | 99 ++++- .../app/module/annotation/service/editor.py | 396 +++++++++++++++--- 10 files changed, 915 insertions(+), 171 deletions(-) create mode 100644 runtime/datamate-python/alembic.ini create mode 100644 runtime/datamate-python/alembic/env.py create mode 100644 runtime/datamate-python/alembic/script.py.mako create mode 100644 runtime/datamate-python/alembic/versions/20250205_0001_add_file_version.py diff --git a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx index 18b2a21..5f3192c 100644 --- a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx +++ b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx @@ -8,6 +8,9 @@ import { getEditorTaskUsingGet, listEditorTasksUsingGet, upsertEditorAnnotationUsingPut, + checkFileVersionUsingGet, + useNewVersionUsingPost, + type FileVersionCheckResponse, } from "../annotation.api"; import { AnnotationResultStatus } from "../annotation.model"; @@ -269,6 +272,11 @@ export default function LabelStudioTextEditor() { return Array.from({ length: segmentTotal }, (_, index) => index); }, [segmentTotal]); + // 文件版本相关状态 + const [fileVersionInfo, setFileVersionInfo] = useState(null); + const [checkingFileVersion, setCheckingFileVersion] = useState(false); + const [usingNewVersion, setUsingNewVersion] = useState(false); + const focusIframe = useCallback(() => { const iframe = iframeRef.current; if (!iframe) return; @@ -548,6 +556,77 @@ export default function LabelStudioTextEditor() { } }, [iframeReady, message, postToIframe, project, projectId]); + const checkFileVersion = useCallback(async (fileId: string) => { + if (!projectId || !fileId) return; + setCheckingFileVersion(true); + try { + const resp = (await checkFileVersionUsingGet(projectId, fileId)) as ApiResponse; + const data = resp?.data; + if (data) { + setFileVersionInfo(data); + if (data.hasNewVersion) { + modal.warning({ + title: "文件有新版本", + content: ( +
+ + 文件已更新到新版本(当前版本: {data.currentFileVersion},标注版本: {data.annotationFileVersion})。 + + + 点击"使用新版本"可清空当前标注并使用最新版本的文件内容。 + +
+ ), + okText: "我知道了", + }); + } + } + } catch (e) { + console.error("检查文件版本失败", e); + } finally { + setCheckingFileVersion(false); + } + }, [modal, message, projectId]); + + const handleUseNewVersion = useCallback(async () => { + if (!selectedFileId) return; + + modal.confirm({ + title: "确认使用新版本", + content: ( +
+ + 确认使用新版本?这将清空当前标注并使用最新版本的文件内容。 + + {fileVersionInfo && ( + + 当前标注版本: {fileVersionInfo.annotationFileVersion},最新文件版本: {fileVersionInfo.currentFileVersion} + + )} +
+ ), + okText: "确认", + okType: "danger", + cancelText: "取消", + onOk: async () => { + if (!projectId || !selectedFileId) return; + setUsingNewVersion(true); + try { + await useNewVersionUsingPost(projectId, selectedFileId); + message.success("已使用新版本并清空标注"); + setFileVersionInfo(null); + await loadTasks({ mode: "reset" }); + await initEditorForFile(selectedFileId); + } catch (e) { + console.error("使用新版本失败", e); + message.error("使用新版本失败"); + } finally { + setUsingNewVersion(false); + } + }, + }); + }, [modal, message, projectId, selectedFileId, fileVersionInfo, loadTasks, initEditorForFile]); + const advanceAfterSave = useCallback(async (fileId: string, segmentIndex?: number) => { if (!fileId) return; if (segmented && segmentTotal > 0) { @@ -815,6 +894,13 @@ export default function LabelStudioTextEditor() { return () => window.removeEventListener("message", handler); }, [message, origin, saveFromExport]); + useEffect(() => { + if (selectedFileId && project?.supported) { + setFileVersionInfo(null); + checkFileVersion(selectedFileId); + } + }, [selectedFileId, project?.supported, checkFileVersion]); + const canLoadMore = taskTotalPages > 0 && taskPage + 1 < taskTotalPages; const saveDisabled = !iframeReady || !selectedFileId || saving || loadingTaskDetail; @@ -896,6 +982,22 @@ export default function LabelStudioTextEditor() {
+ {fileVersionInfo?.hasNewVersion && ( +
+ + ⚠ 文件有新版本({fileVersionInfo.currentFileVersion} > {fileVersionInfo.annotationFileVersion}) + + +
+ )}