You've already forked DataMate
feat(annotation): 添加标注状态管理功能
- 引入 AnnotationResultStatus 枚举类型区分已标注和无标注状态 - 在前端组件中实现空标注检测和确认对话框逻辑 - 添加数据库表字段 annotation_status 存储标注状态 - 扩展后端服务验证和处理标注状态逻辑 - 更新 API 接口支持标注状态参数传递 - 改进任务列表显示逻辑以反映不同标注状态 - 实现分段模式下的标注结果检查机制
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
listEditorTasksUsingGet,
|
||||
upsertEditorAnnotationUsingPut,
|
||||
} from "../annotation.api";
|
||||
import { AnnotationResultStatus } from "../annotation.model";
|
||||
|
||||
type EditorProjectInfo = {
|
||||
projectId: string;
|
||||
@@ -26,6 +27,7 @@ type EditorTaskListItem = {
|
||||
fileType?: string | null;
|
||||
hasAnnotation: boolean;
|
||||
annotationUpdatedAt?: string | null;
|
||||
annotationStatus?: AnnotationResultStatus | null;
|
||||
};
|
||||
|
||||
type LsfMessage = {
|
||||
@@ -88,6 +90,10 @@ type SwitchDecision = "save" | "discard" | "cancel";
|
||||
const LSF_IFRAME_SRC = "/lsf/lsf.html";
|
||||
const TASK_PAGE_START = 0;
|
||||
const TASK_PAGE_SIZE = 200;
|
||||
const NO_ANNOTATION_LABEL = "无标注/不适用";
|
||||
const NO_ANNOTATION_CONFIRM_TITLE = "没有标注任何内容";
|
||||
const NO_ANNOTATION_CONFIRM_OK_TEXT = "设为无标注并保存";
|
||||
const NO_ANNOTATION_CONFIRM_CANCEL_TEXT = "继续标注";
|
||||
|
||||
type NormalizedTaskList = {
|
||||
items: EditorTaskListItem[];
|
||||
@@ -119,6 +125,24 @@ const resolvePayloadMessage = (payload: unknown) => {
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === "object" && !Array.isArray(value);
|
||||
|
||||
const isAnnotationResultEmpty = (annotation?: Record<string, unknown>) => {
|
||||
if (!annotation) return true;
|
||||
if (!("result" in annotation)) return true;
|
||||
const result = (annotation as { result?: unknown }).result;
|
||||
if (!Array.isArray(result)) return false;
|
||||
return result.length === 0;
|
||||
};
|
||||
|
||||
const resolveTaskStatusMeta = (item: EditorTaskListItem) => {
|
||||
if (!item.hasAnnotation) {
|
||||
return { text: "未标注", type: "secondary" as const };
|
||||
}
|
||||
if (item.annotationStatus === AnnotationResultStatus.NO_ANNOTATION) {
|
||||
return { text: NO_ANNOTATION_LABEL, type: "warning" as const };
|
||||
}
|
||||
return { text: "已标注", type: "success" as const };
|
||||
};
|
||||
|
||||
const normalizeSnapshotValue = (value: unknown, seen: WeakSet<object>): unknown => {
|
||||
if (!value || typeof value !== "object") return value;
|
||||
const obj = value as object;
|
||||
@@ -247,6 +271,30 @@ export default function LabelStudioTextEditor() {
|
||||
win.postMessage({ type, payload }, origin);
|
||||
}, [origin]);
|
||||
|
||||
const confirmNoAnnotation = useCallback(() => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
let resolved = false;
|
||||
const settle = (value: boolean) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
resolve(value);
|
||||
};
|
||||
modal.confirm({
|
||||
title: NO_ANNOTATION_CONFIRM_TITLE,
|
||||
content: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography.Text>当前未发现任何标注内容。</Typography.Text>
|
||||
<Typography.Text type="secondary">如确认为无标注/不适用,可继续保存。</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
okText: NO_ANNOTATION_CONFIRM_OK_TEXT,
|
||||
cancelText: NO_ANNOTATION_CONFIRM_CANCEL_TEXT,
|
||||
onOk: () => settle(true),
|
||||
onCancel: () => settle(false),
|
||||
});
|
||||
});
|
||||
}, [modal]);
|
||||
|
||||
const loadProject = useCallback(async () => {
|
||||
setLoadingProject(true);
|
||||
try {
|
||||
@@ -539,11 +587,27 @@ export default function LabelStudioTextEditor() {
|
||||
? currentSegmentIndex
|
||||
: undefined;
|
||||
|
||||
const annotationRecord = annotation as Record<string, unknown>;
|
||||
let resolvedStatus: AnnotationResultStatus;
|
||||
if (isAnnotationResultEmpty(annotationRecord)) {
|
||||
const currentStatus = tasks.find((item) => item.fileId === String(fileId))?.annotationStatus;
|
||||
if (currentStatus === AnnotationResultStatus.NO_ANNOTATION) {
|
||||
resolvedStatus = AnnotationResultStatus.NO_ANNOTATION;
|
||||
} else {
|
||||
const confirmed = await confirmNoAnnotation();
|
||||
if (!confirmed) return false;
|
||||
resolvedStatus = AnnotationResultStatus.NO_ANNOTATION;
|
||||
}
|
||||
} else {
|
||||
resolvedStatus = AnnotationResultStatus.ANNOTATED;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const resp = (await upsertEditorAnnotationUsingPut(projectId, String(fileId), {
|
||||
annotation,
|
||||
segmentIndex,
|
||||
annotationStatus: resolvedStatus,
|
||||
})) as ApiResponse<UpsertAnnotationResponse>;
|
||||
const updatedAt = resp?.data?.updatedAt;
|
||||
message.success("标注已保存");
|
||||
@@ -553,6 +617,7 @@ export default function LabelStudioTextEditor() {
|
||||
? {
|
||||
...item,
|
||||
hasAnnotation: true,
|
||||
annotationStatus: resolvedStatus,
|
||||
annotationUpdatedAt: updatedAt || item.annotationUpdatedAt,
|
||||
}
|
||||
: item
|
||||
@@ -586,11 +651,13 @@ export default function LabelStudioTextEditor() {
|
||||
}
|
||||
}, [
|
||||
advanceAfterSave,
|
||||
confirmNoAnnotation,
|
||||
currentSegmentIndex,
|
||||
message,
|
||||
projectId,
|
||||
segmented,
|
||||
selectedFileId,
|
||||
tasks,
|
||||
]);
|
||||
|
||||
const requestExportForCheck = useCallback(() => {
|
||||
@@ -1016,37 +1083,37 @@ export default function LabelStudioTextEditor() {
|
||||
size="small"
|
||||
dataSource={tasks}
|
||||
loadMore={loadMoreNode}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
key={item.fileId}
|
||||
className="cursor-pointer hover:bg-blue-50"
|
||||
style={{
|
||||
background: item.fileId === selectedFileId ? "#e6f4ff" : undefined,
|
||||
padding: "8px 12px",
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
}}
|
||||
onClick={() => setSelectedFileId(item.fileId)}
|
||||
>
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<Typography.Text ellipsis style={{ fontSize: 13 }}>
|
||||
{item.fileName}
|
||||
</Typography.Text>
|
||||
<div className="flex items-center justify-between">
|
||||
<Typography.Text
|
||||
type={item.hasAnnotation ? "success" : "secondary"}
|
||||
style={{ fontSize: 11 }}
|
||||
>
|
||||
{item.hasAnnotation ? "已标注" : "未标注"}
|
||||
renderItem={(item) => {
|
||||
const statusMeta = resolveTaskStatusMeta(item);
|
||||
return (
|
||||
<List.Item
|
||||
key={item.fileId}
|
||||
className="cursor-pointer hover:bg-blue-50"
|
||||
style={{
|
||||
background: item.fileId === selectedFileId ? "#e6f4ff" : undefined,
|
||||
padding: "8px 12px",
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
}}
|
||||
onClick={() => setSelectedFileId(item.fileId)}
|
||||
>
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<Typography.Text ellipsis style={{ fontSize: 13 }}>
|
||||
{item.fileName}
|
||||
</Typography.Text>
|
||||
{item.annotationUpdatedAt && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 10 }}>
|
||||
{item.annotationUpdatedAt}
|
||||
<div className="flex items-center justify-between">
|
||||
<Typography.Text type={statusMeta.type} style={{ fontSize: 11 }}>
|
||||
{statusMeta.text}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{item.annotationUpdatedAt && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 10 }}>
|
||||
{item.annotationUpdatedAt}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{segmented && (
|
||||
|
||||
@@ -8,6 +8,11 @@ export enum AnnotationTaskStatus {
|
||||
SKIPPED = "skipped",
|
||||
}
|
||||
|
||||
export enum AnnotationResultStatus {
|
||||
ANNOTATED = "ANNOTATED",
|
||||
NO_ANNOTATION = "NO_ANNOTATION",
|
||||
}
|
||||
|
||||
export interface AnnotationTask {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -52,7 +57,7 @@ export interface ObjectDefinition {
|
||||
export interface TemplateConfiguration {
|
||||
labels: LabelDefinition[];
|
||||
objects: ObjectDefinition[];
|
||||
metadata?: Record<string, any>;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AnnotationTemplate {
|
||||
|
||||
Reference in New Issue
Block a user