Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions frontend/src/components/business/DatasetFileTransfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
...item,
id: item.id,
key: String(item.id), // rowKey 使用字符串,确保与 selectedRowKeys 类型一致
// 记录所属数据集,方便后续在“全不选”时只清空当前数据集的选择
// DatasetFile 接口是后端模型,这里在前端扩展 datasetId 字段
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
datasetId: selectedDataset.id,
datasetName: selectedDataset.name,
}))
);
Expand Down Expand Up @@ -176,6 +181,10 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
(item: DatasetFile) => ({
...item,
key: item.id,
// 同样为批量全选结果打上 datasetId 标记
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
datasetId: selectedDataset.id,
datasetName: selectedDataset.name,
}),
);
Expand Down Expand Up @@ -260,9 +269,7 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
<div className="flex items-center gap-2">
<span
className={`inline-flex h-3 w-3 rounded-full border transition-colors duration-150 ${
active
? "border-blue-500 bg-blue-500 shadow-[0_0_0_2px_rgba(59,130,246,0.25)]"
: "border-gray-300 bg-white"
active ? "border-blue-500 bg-blue-500" : "border-gray-300 bg-white"
}`}
/>
<span className="truncate" title={text}>
Expand Down Expand Up @@ -394,8 +401,20 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
// 而不是只选中当前页
handleSelectAllInDataset();
} else {
// 取消表头“全选”时,清空当前已选文件
onSelectedFilesChange({});
// 取消表头“全选”时,只清空当前数据集的已选文件,保留其它数据集
if (!selectedDataset) return;

const nextMap: { [key: string]: DatasetFile } = {};
Object.entries(selectedFilesMap).forEach(([key, file]) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const fileDatasetId = file.datasetId;
if (fileDatasetId !== selectedDataset.id) {
nextMap[key] = file;
}
});

onSelectedFilesChange(nextMap);
}
},

Expand Down
109 changes: 107 additions & 2 deletions frontend/src/pages/DataAnnotation/AutoAnnotation/AutoAnnotation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import {
DownloadOutlined,
ReloadOutlined,
EyeOutlined,
SyncOutlined,
EditOutlined,
} from "@ant-design/icons";
import type { ColumnType } from "antd/es/table";
import type { AutoAnnotationTask, AutoAnnotationStatus } from "../annotation.model";
import {
queryAutoAnnotationTasksUsingGet,
deleteAutoAnnotationTaskByIdUsingDelete,
downloadAutoAnnotationResultUsingGet,
queryAnnotationTasksUsingGet,
syncAutoAnnotationTaskToLabelStudioUsingPost,
} from "../annotation.api";
import CreateAutoAnnotationDialog from "./components/CreateAutoAnnotationDialog";

Expand Down Expand Up @@ -45,6 +49,8 @@ export default function AutoAnnotation() {
const [tasks, setTasks] = useState<AutoAnnotationTask[]>([]);
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const [labelStudioBase, setLabelStudioBase] = useState<string | null>(null);
const [datasetProjectMap, setDatasetProjectMap] = useState<Record<string, string>>({});

useEffect(() => {
fetchTasks();
Expand All @@ -54,6 +60,39 @@ export default function AutoAnnotation() {
return () => clearInterval(interval);
}, []);

// 预取 Label Studio 基础 URL 和数据集到项目的映射
useEffect(() => {
let mounted = true;
(async () => {
try {
const baseUrl = `http://${window.location.hostname}:${parseInt(window.location.port) + 1}`;
if (mounted) setLabelStudioBase(baseUrl);
} catch (e) {
if (mounted) setLabelStudioBase(null);
}

// 拉取所有标注任务,构建 datasetId -> labelingProjId 映射
try {
const resp = await queryAnnotationTasksUsingGet({ page: 1, size: 1000 } as any);
const content: any[] = (resp as any)?.data?.content || (resp as any)?.data || resp || [];
const map: Record<string, string> = {};
content.forEach((task: any) => {
const datasetId = task.datasetId || task.dataset_id;
const projId = task.labelingProjId || task.projId || task.labeling_project_id;
if (datasetId && projId) {
map[String(datasetId)] = String(projId);
}
});
if (mounted) setDatasetProjectMap(map);
} catch (e) {
console.error("Failed to build dataset->LabelStudio project map:", e);
}
})();
return () => {
mounted = false;
};
}, []);

const fetchTasks = async (silent = false) => {
if (!silent) setLoading(true);
try {
Expand Down Expand Up @@ -101,6 +140,56 @@ export default function AutoAnnotation() {
}
};

const handleSyncToLabelStudio = (task: AutoAnnotationTask) => {
if (task.status !== "completed") {
message.warning("仅已完成的任务可以同步到 Label Studio");
return;
}

Modal.confirm({
title: `确认同步自动标注任务「${task.name}」到 Label Studio 吗?`,
content: (
<div>
<div>将把该任务的检测结果作为预测框写入 Label Studio。</div>
<div>不会覆盖已有人工标注,仅作为可编辑的预测结果。</div>
</div>
),
okText: "同步",
cancelText: "取消",
onOk: async () => {
try {
await syncAutoAnnotationTaskToLabelStudioUsingPost(task.id);
message.success("同步请求已发送");
} catch (error) {
console.error(error);
message.error("同步失败,请稍后重试");
}
},
});
};

const handleAnnotate = (task: AutoAnnotationTask) => {
const datasetId = task.datasetId;
if (!datasetId) {
message.error("该任务未绑定数据集,无法跳转 Label Studio");
return;
}

const projId = datasetProjectMap[String(datasetId)];
if (!projId) {
message.error("未找到对应的标注工程,请先为该数据集创建手动标注任务");
return;
}

if (!labelStudioBase) {
message.error("无法跳转到 Label Studio:未配置 Label Studio 基础 URL");
return;
}

const target = `${labelStudioBase}/projects/${projId}/data`;
window.open(target, "_blank");
};

const handleViewResult = (task: AutoAnnotationTask) => {
if (task.outputPath) {
Modal.info({
Expand Down Expand Up @@ -214,7 +303,7 @@ export default function AutoAnnotation() {
{
title: "操作",
key: "actions",
width: 180,
width: 260,
fixed: "right",
render: (_: any, record: AutoAnnotationTask) => (
<Space size="small">
Expand All @@ -236,9 +325,25 @@ export default function AutoAnnotation() {
onClick={() => handleDownload(record)}
/>
</Tooltip>
<Tooltip title="同步到 Label Studio">
<Button
type="link"
size="small"
icon={<SyncOutlined />}
onClick={() => handleSyncToLabelStudio(record)}
/>
</Tooltip>
<Tooltip title="在 Label Studio 中标注">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => handleAnnotate(record)}
/>
</Tooltip>
</>
)}
<Tooltip title="删除">
<Tooltip title="删除任务记录">
<Button
type="link"
size="small"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,20 @@ export default function CreateAutoAnnotationDialog({

setLoading(true);

const selectedFiles = Object.values(selectedFilesMap) as any[];
// 自动标注任务现在允许跨多个数据集,后端会按 fileIds 分组并为每个数据集分别创建/复用 LS 项目。
// 这里仅用第一个涉及到的 datasetId(或表单中的 datasetId)作为任务的“主数据集”展示字段。
const datasetIds = Array.from(
new Set(
selectedFiles
.map((file) => file?.datasetId)
.filter((id) => id !== undefined && id !== null && id !== ""),
),
),
);

const effectiveDatasetId = values.datasetId || datasetIds[0];

const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
const imageFileIds = Object.values(selectedFilesMap)
.filter((file) => {
Expand All @@ -168,7 +182,7 @@ export default function CreateAutoAnnotationDialog({

const payload = {
name: values.name,
datasetId: values.datasetId,
datasetId: effectiveDatasetId,
fileIds: imageFileIds,
config: {
modelSize: values.modelSize,
Expand Down
Loading
Loading