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
This commit is contained in:
2026-02-05 20:12:07 +08:00
parent 4143bc75f9
commit f5cb265667
10 changed files with 915 additions and 171 deletions

View File

@@ -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<FileVersionCheckResponse | null>(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<FileVersionCheckResponse>;
const data = resp?.data;
if (data) {
setFileVersionInfo(data);
if (data.hasNewVersion) {
modal.warning({
title: "文件有新版本",
content: (
<div className="flex flex-col gap-2">
<Typography.Text>
: {data.currentFileVersion}: {data.annotationFileVersion}
</Typography.Text>
<Typography.Text type="secondary">
"使用新版本"使
</Typography.Text>
</div>
),
okText: "我知道了",
});
}
}
} catch (e) {
console.error("检查文件版本失败", e);
} finally {
setCheckingFileVersion(false);
}
}, [modal, message, projectId]);
const handleUseNewVersion = useCallback(async () => {
if (!selectedFileId) return;
modal.confirm({
title: "确认使用新版本",
content: (
<div className="flex flex-col gap-2">
<Typography.Text>
使使
</Typography.Text>
{fileVersionInfo && (
<Typography.Text type="secondary">
: {fileVersionInfo.annotationFileVersion}: {fileVersionInfo.currentFileVersion}
</Typography.Text>
)}
</div>
),
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() {
</Typography.Title>
</div>
<div className="flex items-center justify-center">
{fileVersionInfo?.hasNewVersion && (
<div className="flex items-center gap-2 mr-4">
<Typography.Text type="warning" className="text-xs">
{fileVersionInfo.currentFileVersion} > {fileVersionInfo.annotationFileVersion}
</Typography.Text>
<Button
size="small"
type="primary"
danger
loading={usingNewVersion}
onClick={handleUseNewVersion}
>
使
</Button>
</div>
)}
<Button
type="primary"
icon={<SaveOutlined />}