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([]); const [templates, setTemplates] = useState([]); 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>({}); const [selectedDataset, setSelectedDataset] = useState(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 = '\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 += ` \n`; } else { xml += ` '; 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 ( <> } width={800} > setActiveMode(key as "manual" | "auto")} items={[ { key: "manual", label: "手动标注", children: (
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
setNameManuallyEdited(true)} />
{/* 描述变为可选 */}