Skip to content

Commit f4c5d04

Browse files
authored
feat(annotation): simplify task creation and streamline sync workflow (#294)
- Restrict annotation tasks to a single dataset while allowing multi-data selection - Unify sync mechanism by removing forward/backward distinction and only syncing results back to the source dataset - Simplify task actions by keeping primary actions (sync, edit) and moving low-frequency actions to a secondary menu - Store auto-annotation results directly in the source dataset without creating additional image datasets - Enable automatic sync of auto-annotation results to Label Studio upon completion - Refactor annotation task creation UI to improve configuration clarity and data selection flow
1 parent dab469f commit f4c5d04

File tree

16 files changed

+638
-531
lines changed

16 files changed

+638
-531
lines changed

frontend/src/components/business/DatasetFileTransfer.tsx

Lines changed: 149 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,31 @@ interface DatasetFileTransferProps
2121
onSelectedFilesChange: (filesMap: { [key: string]: DatasetFile }) => void;
2222
onDatasetSelect?: (dataset: Dataset | null) => void;
2323
datasetTypeFilter?: DatasetType;
24+
/**
25+
* 是否强制“单数据集模式”:
26+
* - 为 true 时,仅允许从同一个数据集选择文件;
27+
* - 当已选文件来自某个数据集时,尝试从其他数据集勾选文件会被阻止并提示。
28+
*/
29+
singleDatasetOnly?: boolean;
30+
/**
31+
* 固定可选数据集 ID:
32+
* - 设置后,左侧数据集列表只展示该数据集;
33+
* - 主要用于“编辑任务数据集”场景,锁定为任务创建时的数据集。
34+
*/
35+
fixedDatasetId?: string | number;
2436
/**
2537
* 锁定的文件ID集合:
2638
* - 在左侧文件列表中,这些文件的勾选框会变成灰色且不可交互;
2739
* - 点击整行也不会改变其选中状态;
2840
* - 主要用于“编辑任务数据集”场景下锁死任务初始文件。
2941
*/
3042
lockedFileIds?: string[];
43+
/**
44+
* 整体禁用开关:
45+
* - 为 true 时,禁止切换数据集和选择文件,仅用于展示当前配置;
46+
* - 可配合上层逻辑(如“需先选模板再选数据集”)使用。
47+
*/
48+
disabled?: boolean;
3149
}
3250

3351
const fileCols = [
@@ -59,7 +77,10 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
5977
onSelectedFilesChange,
6078
onDatasetSelect,
6179
datasetTypeFilter,
80+
singleDatasetOnly,
81+
fixedDatasetId,
6282
lockedFileIds,
83+
disabled,
6384
...props
6485
}) => {
6586
const [datasets, setDatasets] = React.useState<Dataset[]>([]);
@@ -91,27 +112,57 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
91112
return new Set((lockedFileIds || []).map((id) => String(id)));
92113
}, [lockedFileIds]);
93114

115+
// 在单数据集模式下,根据已选文件反推“当前锁定的数据集ID”
116+
const lockedDatasetId = React.useMemo(() => {
117+
if (!singleDatasetOnly) return undefined;
118+
const ids = new Set(
119+
Object.values(selectedFilesMap)
120+
.map((file: any) => file?.datasetId)
121+
.filter((id) => id !== undefined && id !== null && id !== "")
122+
.map((id) => String(id))
123+
);
124+
if (ids.size === 1) {
125+
return Array.from(ids)[0];
126+
}
127+
return undefined;
128+
}, [singleDatasetOnly, selectedFilesMap]);
129+
94130
const fetchDatasets = async () => {
95131
const { data } = await queryDatasetsUsingGet({
96132
// Ant Design Table pagination.current is 1-based; ensure backend also receives 1-based value
97133
page: datasetPagination.current,
98134
size: datasetPagination.pageSize,
99135
keyword: datasetSearch,
100-
// 仅在显式传入过滤类型时才按类型过滤;否则后端返回所有类型
136+
// 后端在大多数环境下支持按 type 过滤;若未生效,前端仍会基于 datasetTypeFilter 再做一次兜底筛选
101137
type: datasetTypeFilter,
102138
});
103-
setDatasets(data.content.map(mapDataset) || []);
139+
140+
let mapped: any[] = (data.content || []).map(mapDataset);
141+
142+
// 兜底:在前端再按 datasetTypeFilter 过滤一次,确保只展示指定类型的数据集
143+
if (datasetTypeFilter) {
144+
mapped = mapped.filter(
145+
(ds: any) => ds.datasetType === datasetTypeFilter
146+
);
147+
}
148+
149+
const filtered =
150+
fixedDatasetId !== undefined && fixedDatasetId !== null
151+
? mapped.filter((ds: Dataset) => String(ds.id) === String(fixedDatasetId))
152+
: mapped;
153+
154+
setDatasets(filtered);
104155
setDatasetPagination((prev) => ({
105156
...prev,
106-
total: data.totalElements,
157+
total: filtered.length,
107158
}));
108159
};
109160

110161
useDebouncedEffect(
111162
() => {
112163
fetchDatasets();
113164
},
114-
[datasetSearch, datasetPagination.pageSize, datasetPagination.current],
165+
[datasetSearch, datasetPagination.pageSize, datasetPagination.current, datasetTypeFilter],
115166
300
116167
);
117168

@@ -170,12 +221,40 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
170221
onDatasetSelect?.(selectedDataset);
171222
}, [selectedDataset, onDatasetSelect]);
172223

224+
// 在 fixedDatasetId 场景下,数据集列表加载完成后自动选中该数据集
225+
useEffect(() => {
226+
if (!open) return;
227+
if (fixedDatasetId === undefined || fixedDatasetId === null) return;
228+
if (selectedDataset) return;
229+
if (!datasets.length) return;
230+
231+
const target = datasets.find((ds) => String(ds.id) === String(fixedDatasetId));
232+
if (target) {
233+
setSelectedDataset(target);
234+
}
235+
}, [open, fixedDatasetId, datasets, selectedDataset]);
236+
173237
const handleSelectAllInDataset = useCallback(async () => {
174238
if (!selectedDataset) {
175239
message.warning("请先选择一个数据集");
176240
return;
177241
}
178242

243+
// 单数据集模式下,如果当前已选文件来自其他数据集,则阻止一键全选
244+
if (singleDatasetOnly) {
245+
const existingIds = new Set(
246+
Object.values(selectedFilesMap)
247+
.map((file: any) => file?.datasetId)
248+
.filter((id) => id !== undefined && id !== null && id !== "")
249+
.map((id) => String(id)),
250+
);
251+
const currentId = String(selectedDataset.id);
252+
if (existingIds.size > 0 && (!existingIds.has(currentId) || existingIds.size > 1)) {
253+
message.warning("当前仅支持从一个数据集选择文件,请先清空已选文件后再切换数据集");
254+
return;
255+
}
256+
}
257+
179258
try {
180259
setSelectingAll(true);
181260

@@ -246,6 +325,23 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
246325
if (lockedIdSet.has(String(record.id))) {
247326
return;
248327
}
328+
329+
// 单数据集模式:禁止从多个数据集混选文件
330+
if (singleDatasetOnly && !selectedFilesMap[record.id]) {
331+
const recordDatasetId = (record as any).datasetId;
332+
const existingIds = new Set(
333+
Object.values(selectedFilesMap)
334+
.map((file: any) => file?.datasetId)
335+
.filter((id) => id !== undefined && id !== null && id !== "")
336+
.map((id) => String(id)),
337+
);
338+
const recId = recordDatasetId !== undefined && recordDatasetId !== null ? String(recordDatasetId) : undefined;
339+
if (existingIds.size > 0 && recId && !existingIds.has(recId)) {
340+
message.warning("当前仅支持从一个数据集选择文件,请先清空已选文件后再切换数据集");
341+
return;
342+
}
343+
}
344+
249345
if (!selectedFilesMap[record.id]) {
250346
onSelectedFilesChange({
251347
...selectedFilesMap,
@@ -321,35 +417,60 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
321417
placeholder="搜索数据集名称..."
322418
value={datasetSearch}
323419
allowClear
324-
onChange={(e) => setDatasetSearch(e.target.value)}
420+
onChange={(e) => !disabled && setDatasetSearch(e.target.value)}
421+
disabled={disabled}
325422
/>
326423
</div>
327424
<Table
328425
scroll={{ y: 400 }}
329426
rowKey="id"
330427
size="small"
331-
rowClassName={(record) =>
332-
`cursor-pointer ${
333-
selectedDataset?.id === record.id ? "bg-blue-100" : ""
334-
}`
335-
}
428+
rowClassName={(record) => {
429+
const isActive = selectedDataset?.id === record.id;
430+
const hasSelection = Object.keys(selectedFilesMap).length > 0;
431+
const isLockedOtherDataset =
432+
!!singleDatasetOnly &&
433+
!!lockedDatasetId &&
434+
hasSelection &&
435+
String(record.id) !== lockedDatasetId;
436+
return `cursor-pointer ${
437+
isActive ? "bg-blue-100" : ""
438+
} ${isLockedOtherDataset ? "text-gray-400 cursor-not-allowed" : ""}`;
439+
}}
336440
onRow={(record: Dataset) => ({
337441
onClick: () => {
338-
setSelectedDataset(record);
339-
if (!datasetSelections.find((d) => d.id === record.id)) {
340-
setDatasetSelections([...datasetSelections, record]);
341-
} else {
342-
setDatasetSelections(
343-
datasetSelections.filter((d) => d.id !== record.id)
344-
);
345-
}
442+
if (disabled) return;
443+
444+
// 单数据集模式:当已有选中文件且尝试切换到其他数据集时,直接提示并阻止切换
445+
const hasSelection =
446+
singleDatasetOnly &&
447+
Object.keys(selectedFilesMap).length > 0 &&
448+
!!lockedDatasetId;
449+
if (
450+
hasSelection &&
451+
String(record.id) !== String(lockedDatasetId)
452+
) {
453+
message.warning(
454+
"当前仅支持从一个数据集选择文件,请先清空已选文件后再切换数据集"
455+
);
456+
return;
457+
}
458+
setSelectedDataset(record);
459+
if (!datasetSelections.find((d) => d.id === record.id)) {
460+
setDatasetSelections([...datasetSelections, record]);
461+
} else {
462+
setDatasetSelections(
463+
datasetSelections.filter((d) => d.id !== record.id)
464+
);
465+
}
346466
},
347467
})}
348468
dataSource={datasets}
349469
columns={datasetCols}
350470
pagination={{
351471
...datasetPagination,
352472
onChange: (page, pageSize) =>
473+
!disabled &&
353474
setDatasetPagination({
354475
current: page,
355476
pageSize: pageSize || datasetPagination.pageSize,
@@ -365,8 +486,8 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
365486
<Button
366487
type="link"
367488
size="small"
368-
onClick={handleSelectAllInDataset}
369-
disabled={!selectedDataset}
489+
onClick={() => !disabled && handleSelectAllInDataset()}
490+
disabled={!selectedDataset || !!disabled}
370491
loading={selectingAll}
371492
>
372493
全选当前数据集
@@ -388,6 +509,7 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
388509
pagination={{
389510
...filesPagination,
390511
onChange: (page, pageSize) => {
512+
if (disabled) return;
391513
const nextPageSize = pageSize || filesPagination.pageSize;
392514
setFilesPagination((prev) => ({
393515
...prev,
@@ -399,7 +521,10 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
399521
},
400522
}}
401523
onRow={(record: DatasetFile) => ({
402-
onClick: () => toggleSelectFile(record),
524+
onClick: () => {
525+
if (disabled) return;
526+
toggleSelectFile(record);
527+
},
403528
})}
404529
rowSelection={{
405530
type: "checkbox",
@@ -408,11 +533,13 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
408533

409534
// 单选
410535
onSelect: (record: DatasetFile) => {
536+
if (disabled) return;
411537
toggleSelectFile(record);
412538
},
413539

414540
// 全选 - 改为全选整个数据集而不是当前页
415541
onSelectAll: (selected, selectedRows: DatasetFile[]) => {
542+
if (disabled) return;
416543
if (selected) {
417544
// 点击表头“全选”时,改为一键全选当前数据集的全部文件
418545
// 而不是只选中当前页
@@ -437,7 +564,7 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
437564

438565
getCheckboxProps: (record: DatasetFile) => ({
439566
name: record.fileName,
440-
disabled: lockedIdSet.has(String(record.id)),
567+
disabled: !!disabled || lockedIdSet.has(String(record.id)),
441568
}),
442569
}}
443570
/>

frontend/src/pages/DataAnnotation/AutoAnnotation/AutoAnnotation.tsx

Lines changed: 7 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,15 @@ import {
99
EditOutlined,
1010
MoreOutlined,
1111
SettingOutlined,
12-
ExportOutlined,
13-
ImportOutlined,
12+
SyncOutlined,
1413
} from "@ant-design/icons";
1514
import type { ColumnType } from "antd/es/table";
1615
import type { AutoAnnotationTask, AutoAnnotationStatus } from "../annotation.model";
1716
import {
1817
queryAutoAnnotationTasksUsingGet,
1918
deleteAutoAnnotationTaskByIdUsingDelete,
2019
downloadAutoAnnotationResultUsingGet,
21-
queryAnnotationTasksUsingGet,
22-
syncAutoAnnotationTaskToLabelStudioUsingPost,
20+
queryAnnotationTasksUsingGet,
2321
} from "../annotation.api";
2422
import CreateAutoAnnotationDialog from "./components/CreateAutoAnnotationDialog";
2523
import EditAutoAnnotationDatasetDialog from "./components/EditAutoAnnotationDatasetDialog";
@@ -159,34 +157,6 @@ export default function AutoAnnotation() {
159157
}
160158
};
161159

162-
const handleSyncToLabelStudio = (task: AutoAnnotationTask) => {
163-
if (task.status !== "completed") {
164-
message.warning("仅已完成的任务可以同步到 Label Studio");
165-
return;
166-
}
167-
168-
Modal.confirm({
169-
title: `确认同步自动标注任务「${task.name}」到 Label Studio 吗?`,
170-
content: (
171-
<div>
172-
<div>将把该任务的检测结果作为预测框写入 Label Studio。</div>
173-
<div>不会覆盖已有人工标注,仅作为可编辑的预测结果。</div>
174-
</div>
175-
),
176-
okText: "同步",
177-
cancelText: "取消",
178-
onOk: async () => {
179-
try {
180-
await syncAutoAnnotationTaskToLabelStudioUsingPost(task.id);
181-
message.success("同步请求已发送");
182-
} catch (error) {
183-
console.error(error);
184-
message.error("同步失败,请稍后重试");
185-
}
186-
},
187-
});
188-
};
189-
190160
const handleAnnotate = (task: AutoAnnotationTask) => {
191161
const datasetId = task.datasetId;
192162
if (!datasetId) {
@@ -322,21 +292,11 @@ export default function AutoAnnotation() {
322292
{
323293
title: "操作",
324294
key: "actions",
325-
width: 320,
295+
width: 300,
326296
fixed: "right",
327297
render: (_: any, record: AutoAnnotationTask) => (
328298
<Space size="small">
329-
{/* 一级功能菜单:前向同步 + 编辑(跳转 Label Studio) */}
330-
<Tooltip title="将 YOLO 预测结果前向同步到 Label Studio">
331-
<Button
332-
type="link"
333-
size="small"
334-
icon={<ExportOutlined />}
335-
onClick={() => handleSyncToLabelStudio(record)}
336-
>
337-
前向同步
338-
</Button>
339-
</Tooltip>
299+
{/* 一级功能:编辑(跳转 Label Studio) + 同步(导回结果) */}
340300
<Tooltip title="在 Label Studio 中手动标注">
341301
<Button
342302
type="link"
@@ -347,14 +307,14 @@ export default function AutoAnnotation() {
347307
编辑
348308
</Button>
349309
</Tooltip>
350-
<Tooltip title="从 Label Studio 导回标注结果到数据集">
310+
<Tooltip title="从 Label Studio 同步标注结果到数据集">
351311
<Button
352312
type="link"
353313
size="small"
354-
icon={<ImportOutlined />}
314+
icon={<SyncOutlined />}
355315
onClick={() => handleImportFromLabelStudio(record)}
356316
>
357-
后向同步
317+
同步
358318
</Button>
359319
</Tooltip>
360320

0 commit comments

Comments
 (0)