You've already forked DataMate
- 新增模板编辑标签页支持可视化和XML两种模式 - 实现从表单值同步生成XML配置的功能 - 添加模板选择时自动加载配置到表单的逻辑 - 重构配置模式切换逻辑并优化预览功能 - 将XML编辑器替换为带标签页的可视化配置界面 - 更新模板加载提示信息以反映新的配置方式
734 lines
29 KiB
TypeScript
734 lines
29 KiB
TypeScript
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
|
|
import { mapDataset } from "@/pages/DataManagement/dataset.const";
|
|
import { Button, Form, Input, Modal, Select, message, Tabs, Slider, Checkbox, Radio, Space, Typography } from "antd";
|
|
import TextArea from "antd/es/input/TextArea";
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
createAnnotationTaskUsingPost,
|
|
queryAnnotationTemplatesUsingGet,
|
|
createAutoAnnotationTaskUsingPost,
|
|
} from "../../annotation.api";
|
|
import DatasetFileTransfer from "@/components/business/DatasetFileTransfer";
|
|
import { DatasetType, type Dataset, type DatasetFile } from "@/pages/DataManagement/dataset.model";
|
|
import type { AnnotationTemplate } from "../../annotation.model";
|
|
import LabelStudioEmbed from "@/components/business/LabelStudioEmbed";
|
|
import TemplateConfigurationForm from "../../components/TemplateConfigurationForm";
|
|
|
|
const { Option } = Select;
|
|
|
|
const COCO_CLASSES = [
|
|
// ... (keep existing COCO_CLASSES)
|
|
{ id: 0, name: "person", label: "人" },
|
|
{ id: 1, name: "bicycle", label: "自行车" },
|
|
{ id: 2, name: "car", label: "汽车" },
|
|
{ id: 3, name: "motorcycle", label: "摩托车" },
|
|
{ id: 4, name: "airplane", label: "飞机" },
|
|
{ id: 5, name: "bus", label: "公交车" },
|
|
{ id: 6, name: "train", label: "火车" },
|
|
{ id: 7, name: "truck", label: "卡车" },
|
|
{ id: 8, name: "boat", label: "船" },
|
|
{ id: 9, name: "traffic light", label: "红绿灯" },
|
|
{ id: 10, name: "fire hydrant", label: "消防栓" },
|
|
{ id: 11, name: "stop sign", label: "停止标志" },
|
|
{ id: 12, name: "parking meter", label: "停车计时器" },
|
|
{ id: 13, name: "bench", label: "长椅" },
|
|
{ id: 14, name: "bird", label: "鸟" },
|
|
{ id: 15, name: "cat", label: "猫" },
|
|
{ id: 16, name: "dog", label: "狗" },
|
|
{ id: 17, name: "horse", label: "马" },
|
|
{ id: 18, name: "sheep", label: "羊" },
|
|
{ id: 19, name: "cow", label: "牛" },
|
|
{ id: 20, name: "elephant", label: "大象" },
|
|
{ id: 21, name: "bear", label: "熊" },
|
|
{ id: 22, name: "zebra", label: "斑马" },
|
|
{ id: 23, name: "giraffe", label: "长颈鹿" },
|
|
{ id: 24, name: "backpack", label: "背包" },
|
|
{ id: 25, name: "umbrella", label: "雨伞" },
|
|
{ id: 26, name: "handbag", label: "手提包" },
|
|
{ id: 27, name: "tie", label: "领带" },
|
|
{ id: 28, name: "suitcase", label: "行李箱" },
|
|
{ id: 29, name: "frisbee", label: "飞盘" },
|
|
{ id: 30, name: "skis", label: "滑雪板" },
|
|
{ id: 31, name: "snowboard", label: "滑雪板" },
|
|
{ id: 32, name: "sports ball", label: "球类" },
|
|
{ id: 33, name: "kite", label: "风筝" },
|
|
{ id: 34, name: "baseball bat", label: "棒球棒" },
|
|
{ id: 35, name: "baseball glove", label: "棒球手套" },
|
|
{ id: 36, name: "skateboard", label: "滑板" },
|
|
{ id: 37, name: "surfboard", label: "冲浪板" },
|
|
{ id: 38, name: "tennis racket", label: "网球拍" },
|
|
{ id: 39, name: "bottle", label: "瓶子" },
|
|
{ id: 40, name: "wine glass", label: "酒杯" },
|
|
{ id: 41, name: "cup", label: "杯子" },
|
|
{ id: 42, name: "fork", label: "叉子" },
|
|
{ id: 43, name: "knife", label: "刀" },
|
|
{ id: 44, name: "spoon", label: "勺子" },
|
|
{ id: 45, name: "bowl", label: "碗" },
|
|
{ id: 46, name: "banana", label: "香蕉" },
|
|
{ id: 47, name: "apple", label: "苹果" },
|
|
{ id: 48, name: "sandwich", label: "三明治" },
|
|
{ id: 49, name: "orange", label: "橙子" },
|
|
{ id: 50, name: "broccoli", label: "西兰花" },
|
|
{ id: 51, name: "carrot", label: "胡萝卜" },
|
|
{ id: 52, name: "hot dog", label: "热狗" },
|
|
{ id: 53, name: "pizza", label: "披萨" },
|
|
{ id: 54, name: "donut", label: "甜甜圈" },
|
|
{ id: 55, name: "cake", label: "蛋糕" },
|
|
{ id: 56, name: "chair", label: "椅子" },
|
|
{ id: 57, name: "couch", label: "沙发" },
|
|
{ id: 58, name: "potted plant", label: "盆栽" },
|
|
{ id: 59, name: "bed", label: "床" },
|
|
{ id: 60, name: "dining table", label: "餐桌" },
|
|
{ id: 61, name: "toilet", label: "马桶" },
|
|
{ id: 62, name: "tv", label: "电视" },
|
|
{ id: 63, name: "laptop", label: "笔记本电脑" },
|
|
{ id: 64, name: "mouse", label: "鼠标" },
|
|
{ id: 65, name: "remote", label: "遥控器" },
|
|
{ id: 66, name: "keyboard", label: "键盘" },
|
|
{ id: 67, name: "cell phone", label: "手机" },
|
|
{ id: 68, name: "microwave", label: "微波炉" },
|
|
{ id: 69, name: "oven", label: "烤箱" },
|
|
{ id: 70, name: "toaster", label: "烤面包机" },
|
|
{ id: 71, name: "sink", label: "水槽" },
|
|
{ id: 72, name: "refrigerator", label: "冰箱" },
|
|
{ id: 73, name: "book", label: "书" },
|
|
{ id: 74, name: "clock", label: "钟表" },
|
|
{ id: 75, name: "vase", label: "花瓶" },
|
|
{ id: 76, name: "scissors", label: "剪刀" },
|
|
{ id: 77, name: "teddy bear", label: "玩具熊" },
|
|
{ id: 78, name: "hair drier", label: "吹风机" },
|
|
{ id: 79, name: "toothbrush", label: "牙刷" },
|
|
];
|
|
|
|
export default function CreateAnnotationTask({
|
|
open,
|
|
onClose,
|
|
onRefresh,
|
|
}: {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onRefresh: () => void;
|
|
}) {
|
|
const [manualForm] = Form.useForm();
|
|
const [autoForm] = Form.useForm();
|
|
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
|
const [templates, setTemplates] = useState<AnnotationTemplate[]>([]);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
|
|
const [activeMode, setActiveMode] = useState<"manual" | "auto">("manual");
|
|
|
|
// Custom template state
|
|
const [customXml, setCustomXml] = useState("");
|
|
const [showPreview, setShowPreview] = useState(false);
|
|
const [configMode, setConfigMode] = useState<"template" | "custom">("template");
|
|
const [templateEditTab, setTemplateEditTab] = useState<"visual" | "xml">("visual");
|
|
|
|
const [selectAllClasses, setSelectAllClasses] = useState(true);
|
|
const [selectedFilesMap, setSelectedFilesMap] = useState<Record<string, DatasetFile>>({});
|
|
const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(null);
|
|
const [imageFileCount, setImageFileCount] = useState(0);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const fetchData = async () => {
|
|
try {
|
|
// Fetch datasets
|
|
const { data: datasetData } = await queryDatasetsUsingGet({
|
|
page: 0,
|
|
pageSize: 1000,
|
|
});
|
|
setDatasets(datasetData.content.map(mapDataset) || []);
|
|
|
|
// Fetch templates
|
|
const templateResponse = await queryAnnotationTemplatesUsingGet({
|
|
page: 1,
|
|
size: 100,
|
|
});
|
|
|
|
if (templateResponse.code === 200 && templateResponse.data) {
|
|
const fetchedTemplates = templateResponse.data.content || [];
|
|
console.log("Fetched templates:", fetchedTemplates);
|
|
setTemplates(fetchedTemplates);
|
|
} else {
|
|
console.error("Failed to fetch templates:", templateResponse);
|
|
setTemplates([]);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching data:", error);
|
|
setTemplates([]);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, [open]);
|
|
|
|
// Reset form and manual-edit flag when modal opens
|
|
useEffect(() => {
|
|
if (open) {
|
|
manualForm.resetFields();
|
|
autoForm.resetFields();
|
|
setNameManuallyEdited(false);
|
|
setActiveMode("manual");
|
|
setSelectAllClasses(true);
|
|
setSelectedFilesMap({});
|
|
setSelectedDataset(null);
|
|
setImageFileCount(0);
|
|
setCustomXml("");
|
|
setShowPreview(false);
|
|
setConfigMode("template");
|
|
setTemplateEditTab("visual");
|
|
}
|
|
}, [open, manualForm, autoForm]);
|
|
|
|
useEffect(() => {
|
|
const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
|
|
const count = Object.values(selectedFilesMap).filter((file) => {
|
|
const ext = file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
|
|
return imageExtensions.includes(ext);
|
|
}).length;
|
|
setImageFileCount(count);
|
|
}, [selectedFilesMap]);
|
|
|
|
const generateXmlFromConfig = (objects: any[], labels: any[]) => {
|
|
let xml = '<View>\n';
|
|
|
|
// Objects
|
|
if (objects) {
|
|
objects.forEach((obj: any) => {
|
|
xml += ` <${obj.type} name="${obj.name}" value="${obj.value}" />\n`;
|
|
});
|
|
}
|
|
|
|
// Controls
|
|
if (labels) {
|
|
labels.forEach((lbl: any) => {
|
|
let attrs = `name="${lbl.fromName}" toName="${lbl.toName}"`;
|
|
if (lbl.required) attrs += ' required="true"';
|
|
|
|
xml += ` <${lbl.type} ${attrs}>\n`;
|
|
|
|
const options = lbl.type === 'Choices' ? lbl.options : lbl.labels;
|
|
if (options && options.length) {
|
|
options.forEach((opt: string) => {
|
|
if (lbl.type === 'Choices') {
|
|
xml += ` <Choice value="${opt}" />\n`;
|
|
} else {
|
|
xml += ` <Label value="${opt}" />\n`;
|
|
}
|
|
});
|
|
}
|
|
xml += ` </${lbl.type}>\n`;
|
|
});
|
|
}
|
|
|
|
xml += '</View>';
|
|
return xml;
|
|
};
|
|
|
|
// 从表单值同步生成 XML
|
|
const syncFormToXml = () => {
|
|
const objects = manualForm.getFieldValue("objects");
|
|
const labels = manualForm.getFieldValue("labels");
|
|
if (objects && objects.length > 0) {
|
|
const xml = generateXmlFromConfig(objects, labels || []);
|
|
setCustomXml(xml);
|
|
}
|
|
};
|
|
|
|
// 当选择模板时,加载模板配置到表单
|
|
const handleTemplateSelect = (value: string, option: any) => {
|
|
if (option && option.config) {
|
|
setCustomXml(option.config);
|
|
}
|
|
|
|
// 从模板列表中找到完整的模板数据
|
|
const selectedTemplate = templates.find(t => t.id === value);
|
|
if (selectedTemplate?.configuration) {
|
|
const { objects, labels } = selectedTemplate.configuration;
|
|
manualForm.setFieldsValue({
|
|
objects: objects || [{ name: "image", type: "Image", value: "$image" }],
|
|
labels: labels || [],
|
|
});
|
|
} else if (option && option.config) {
|
|
// 如果没有结构化配置,设置默认值
|
|
manualForm.setFieldsValue({
|
|
objects: [{ name: "image", type: "Image", value: "$image" }],
|
|
labels: [],
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleManualSubmit = async () => {
|
|
try {
|
|
const values = await manualForm.validateFields();
|
|
|
|
let finalLabelConfig = "";
|
|
const objects = values.objects;
|
|
const labels = values.labels;
|
|
|
|
if (configMode === "template") {
|
|
// 模板模式:优先使用可视化配置生成 XML,回退到直接使用 XML 编辑器内容
|
|
if (templateEditTab === "visual" && objects && objects.length > 0) {
|
|
finalLabelConfig = generateXmlFromConfig(objects, labels || []);
|
|
} else if (customXml.trim()) {
|
|
finalLabelConfig = customXml;
|
|
} else {
|
|
message.error("请配置标注模板或选择一个现有模板");
|
|
return;
|
|
}
|
|
} else {
|
|
// 自定义模式
|
|
if (!objects || objects.length === 0) {
|
|
message.error("请至少配置一个数据对象");
|
|
return;
|
|
}
|
|
if (!labels || labels.length === 0) {
|
|
message.error("请至少配置一个标签控件");
|
|
return;
|
|
}
|
|
finalLabelConfig = generateXmlFromConfig(objects, labels);
|
|
}
|
|
|
|
setSubmitting(true);
|
|
const requestData = {
|
|
name: values.name,
|
|
description: values.description,
|
|
datasetId: values.datasetId,
|
|
templateId: configMode === 'template' ? values.templateId : undefined,
|
|
labelConfig: finalLabelConfig,
|
|
};
|
|
await createAnnotationTaskUsingPost(requestData);
|
|
message?.success?.("创建标注任务成功");
|
|
onClose();
|
|
onRefresh();
|
|
} catch (err: any) {
|
|
console.error("Create annotation task failed", err);
|
|
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
|
|
(message as any)?.error?.(msg);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleAutoSubmit = async () => {
|
|
// ... (keep existing handleAutoSubmit)
|
|
try {
|
|
const values = await autoForm.validateFields();
|
|
|
|
if (imageFileCount === 0) {
|
|
message.error("请至少选择一个图像文件");
|
|
return;
|
|
}
|
|
|
|
setSubmitting(true);
|
|
|
|
const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
|
|
const imageFileIds = Object.values(selectedFilesMap)
|
|
.filter((file) => {
|
|
const ext = file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
|
|
return imageExtensions.includes(ext);
|
|
})
|
|
.map((file) => file.id);
|
|
|
|
const payload = {
|
|
name: values.name,
|
|
datasetId: values.datasetId,
|
|
fileIds: imageFileIds,
|
|
config: {
|
|
modelSize: values.modelSize,
|
|
confThreshold: values.confThreshold,
|
|
targetClasses: selectAllClasses ? [] : values.targetClasses || [],
|
|
outputDatasetName: values.outputDatasetName || undefined,
|
|
},
|
|
};
|
|
|
|
await createAutoAnnotationTaskUsingPost(payload);
|
|
message.success("自动标注任务创建成功");
|
|
// 触发上层刷新自动标注任务列表
|
|
(onRefresh as any)?.("auto");
|
|
onClose();
|
|
} catch (error: any) {
|
|
if (error.errorFields) return;
|
|
console.error("Failed to create auto annotation task:", error);
|
|
message.error(error.message || "创建自动标注任务失败");
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleClassSelectionChange = (checked: boolean) => {
|
|
setSelectAllClasses(checked);
|
|
if (checked) {
|
|
autoForm.setFieldsValue({ targetClasses: [] });
|
|
}
|
|
};
|
|
|
|
const handleConfigModeChange = (e: any) => {
|
|
const mode = e.target.value;
|
|
setConfigMode(mode);
|
|
// 两种模式都需要初始化默认值
|
|
const currentObjects = manualForm.getFieldValue("objects");
|
|
if (!currentObjects || currentObjects.length === 0) {
|
|
manualForm.setFieldsValue({
|
|
objects: [{ name: "image", type: "Image", value: "$image" }],
|
|
labels: [],
|
|
});
|
|
}
|
|
// 切换到模板模式时,重置 tab 到可视化
|
|
if (mode === "template") {
|
|
setTemplateEditTab("visual");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Modal
|
|
open={open}
|
|
onCancel={onClose}
|
|
title="创建标注任务"
|
|
footer={
|
|
<>
|
|
<Button onClick={onClose} disabled={submitting}>
|
|
取消
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
onClick={activeMode === "manual" ? handleManualSubmit : handleAutoSubmit}
|
|
loading={submitting}
|
|
>
|
|
确定
|
|
</Button>
|
|
</>
|
|
}
|
|
width={800}
|
|
>
|
|
<Tabs
|
|
activeKey={activeMode}
|
|
onChange={(key) => setActiveMode(key as "manual" | "auto")}
|
|
items={[
|
|
{
|
|
key: "manual",
|
|
label: "手动标注",
|
|
children: (
|
|
<Form form={manualForm} layout="vertical">
|
|
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Form.Item
|
|
label="数据集"
|
|
name="datasetId"
|
|
rules={[{ required: true, message: "请选择数据集" }]}
|
|
>
|
|
<Select
|
|
placeholder="请选择数据集"
|
|
options={datasets.map((dataset) => {
|
|
return {
|
|
label: (
|
|
<div className="flex items-center justify-between gap-3 py-2">
|
|
<div className="flex items-center font-sm text-gray-900">
|
|
<span className="mr-2">{(dataset as any).icon}</span>
|
|
<span>{dataset.name}</span>
|
|
</div>
|
|
<div className="text-xs text-gray-500">{dataset.size}</div>
|
|
</div>
|
|
),
|
|
value: dataset.id,
|
|
};
|
|
})}
|
|
onChange={(value) => {
|
|
// 如果用户未手动修改名称,则用数据集名称作为默认任务名
|
|
if (!nameManuallyEdited) {
|
|
const ds = datasets.find((d) => d.id === value);
|
|
if (ds) {
|
|
let defaultName = ds.name || "";
|
|
if (defaultName.length < 3) {
|
|
defaultName = `${defaultName}-标注`;
|
|
}
|
|
manualForm.setFieldsValue({ name: defaultName });
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label="标注工程名称"
|
|
name="name"
|
|
rules={[
|
|
{
|
|
validator: (_rule, value) => {
|
|
const trimmed = (value || "").trim();
|
|
if (!trimmed) {
|
|
return Promise.reject(new Error("请输入任务名称"));
|
|
}
|
|
if (trimmed.length < 3) {
|
|
return Promise.reject(
|
|
new Error("任务名称至少需要 3 个字符(不含首尾空格,Label Studio 限制)"),
|
|
);
|
|
}
|
|
return Promise.resolve();
|
|
},
|
|
},
|
|
]}
|
|
>
|
|
<Input
|
|
placeholder="输入标注工程名称"
|
|
onChange={() => setNameManuallyEdited(true)}
|
|
/>
|
|
</Form.Item>
|
|
</div>
|
|
{/* 描述变为可选 */}
|
|
<Form.Item label="描述" name="description">
|
|
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={2} />
|
|
</Form.Item>
|
|
|
|
{/* 标注模板选择 */}
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium text-gray-700 after:content-['*'] after:text-red-500 after:ml-1">标注配置</span>
|
|
|
|
<div className="flex gap-2">
|
|
<Radio.Group value={configMode} onChange={handleConfigModeChange} size="small" buttonStyle="solid">
|
|
<Radio.Button value="template">现有模板</Radio.Button>
|
|
<Radio.Button value="custom">自定义配置</Radio.Button>
|
|
</Radio.Group>
|
|
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
onClick={() => {
|
|
// 如果在可视化模式,先同步生成 XML
|
|
if (configMode === 'template' && templateEditTab === 'visual') {
|
|
syncFormToXml();
|
|
} else if (configMode === 'custom') {
|
|
// 自定义模式也从表单生成
|
|
const objects = manualForm.getFieldValue("objects");
|
|
const labels = manualForm.getFieldValue("labels");
|
|
if (objects && objects.length > 0) {
|
|
const xml = generateXmlFromConfig(objects, labels || []);
|
|
setCustomXml(xml);
|
|
}
|
|
}
|
|
setShowPreview(true);
|
|
}}
|
|
>
|
|
预览配置效果
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{configMode === 'template' ? (
|
|
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
|
|
<Form.Item
|
|
label="加载现有模板"
|
|
name="templateId"
|
|
style={{ marginBottom: 12 }}
|
|
help="选择模板后,配置将自动填充到可视化编辑器中,您可以继续修改。"
|
|
>
|
|
<Select
|
|
placeholder="选择一个模板作为基础"
|
|
showSearch
|
|
allowClear
|
|
optionFilterProp="label"
|
|
options={templates.map((template) => ({
|
|
label: template.name,
|
|
value: template.id,
|
|
title: template.description,
|
|
config: template.labelConfig,
|
|
}))}
|
|
onChange={handleTemplateSelect}
|
|
optionRender={(option) => (
|
|
<div>
|
|
<div style={{ fontWeight: 500 }}>{option.label}</div>
|
|
{option.data.title && (
|
|
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
|
|
{option.data.title}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Tabs
|
|
activeKey={templateEditTab}
|
|
onChange={(key) => {
|
|
// 切换到 XML 时,从表单同步生成 XML
|
|
if (key === "xml") {
|
|
syncFormToXml();
|
|
}
|
|
setTemplateEditTab(key as "visual" | "xml");
|
|
}}
|
|
size="small"
|
|
items={[
|
|
{
|
|
key: "visual",
|
|
label: "可视化配置",
|
|
children: (
|
|
<div style={{ maxHeight: '350px', overflowY: 'auto' }}>
|
|
<TemplateConfigurationForm form={manualForm} />
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "xml",
|
|
label: "XML编辑器(高级)",
|
|
children: (
|
|
<div>
|
|
<div className="mb-2 text-xs text-gray-500">
|
|
直接编辑 Label Studio XML 配置。注意:在此修改后切换回可视化配置可能会丢失部分高级设置。
|
|
</div>
|
|
<TextArea
|
|
rows={10}
|
|
value={customXml}
|
|
onChange={(e) => setCustomXml(e.target.value)}
|
|
placeholder="<View>...</View>"
|
|
style={{ fontFamily: 'monospace', fontSize: 12 }}
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="bg-gray-50 p-4 rounded-md border border-gray-200" style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
|
<TemplateConfigurationForm form={manualForm} />
|
|
</div>
|
|
)}
|
|
|
|
</Form>
|
|
),
|
|
},
|
|
{
|
|
key: "auto",
|
|
label: "自动标注",
|
|
children: (
|
|
<Form form={autoForm} layout="vertical" preserve={false}>
|
|
<Form.Item
|
|
name="name"
|
|
label="任务名称"
|
|
rules={[
|
|
{ required: true, message: "请输入任务名称" },
|
|
{ max: 100, message: "任务名称不能超过100个字符" },
|
|
]}
|
|
>
|
|
<Input placeholder="请输入任务名称" />
|
|
</Form.Item>
|
|
|
|
<Form.Item label="选择数据集和图像文件" required>
|
|
<DatasetFileTransfer
|
|
open
|
|
selectedFilesMap={selectedFilesMap}
|
|
onSelectedFilesChange={setSelectedFilesMap}
|
|
onDatasetSelect={(dataset) => {
|
|
setSelectedDataset(dataset as Dataset | null);
|
|
autoForm.setFieldsValue({ datasetId: dataset?.id ?? "" });
|
|
}}
|
|
datasetTypeFilter={DatasetType.IMAGE}
|
|
/>
|
|
{selectedDataset && (
|
|
<div className="mt-2 p-2 bg-blue-50 rounded border border-blue-200 text-xs">
|
|
当前数据集:<span className="font-medium">{selectedDataset.name}</span> - 已选择
|
|
<span className="font-medium text-blue-600"> {imageFileCount} </span>个图像文件
|
|
</div>
|
|
)}
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
hidden
|
|
name="datasetId"
|
|
rules={[{ required: true, message: "请选择数据集" }]}
|
|
>
|
|
<Input type="hidden" />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="modelSize"
|
|
label="模型规模"
|
|
rules={[{ required: true, message: "请选择模型规模" }]}
|
|
initialValue="l"
|
|
>
|
|
<Select>
|
|
<Option value="n">YOLOv8n (最快)</Option>
|
|
<Option value="s">YOLOv8s</Option>
|
|
<Option value="m">YOLOv8m</Option>
|
|
<Option value="l">YOLOv8l (推荐)</Option>
|
|
<Option value="x">YOLOv8x (最精确)</Option>
|
|
</Select>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="confThreshold"
|
|
label="置信度阈值"
|
|
rules={[{ required: true, message: "请选择置信度阈值" }]}
|
|
initialValue={0.7}
|
|
>
|
|
<Slider
|
|
min={0.1}
|
|
max={0.9}
|
|
step={0.05}
|
|
tooltip={{ formatter: (v) => `${(v || 0) * 100}%` }}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item label="目标类别">
|
|
<Checkbox
|
|
checked={selectAllClasses}
|
|
onChange={(e) => handleClassSelectionChange(e.target.checked)}
|
|
>
|
|
选中所有类别
|
|
</Checkbox>
|
|
{!selectAllClasses && (
|
|
<Form.Item name="targetClasses" noStyle>
|
|
<Select mode="multiple" placeholder="选择目标类别" style={{ marginTop: 8 }}>
|
|
{COCO_CLASSES.map((cls) => (
|
|
<Option key={cls.id} value={cls.id}>
|
|
{cls.label} ({cls.name})
|
|
</Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
)}
|
|
</Form.Item>
|
|
|
|
<Form.Item name="outputDatasetName" label="输出数据集名称 (可选)">
|
|
<Input placeholder="留空则将结果写入原数据集的标签中" />
|
|
</Form.Item>
|
|
</Form>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</Modal>
|
|
|
|
{/* Preview Modal */}
|
|
<Modal
|
|
open={showPreview}
|
|
onCancel={() => setShowPreview(false)}
|
|
title="标注界面预览"
|
|
width={1000}
|
|
footer={[
|
|
<Button key="close" onClick={() => setShowPreview(false)}>
|
|
关闭
|
|
</Button>
|
|
]}
|
|
>
|
|
<div style={{ height: '600px', overflow: 'hidden' }}>
|
|
{showPreview && (
|
|
<LabelStudioEmbed
|
|
config={customXml}
|
|
task={{
|
|
id: 1,
|
|
data: {
|
|
image: "https://labelstud.io/images/opa-header.png",
|
|
text: "这是示例文本,用于预览标注界面。",
|
|
audio: "https://labelstud.io/files/sample.wav"
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
|