import { queryDatasetsUsingGet, queryDatasetFilesUsingGet } from "@/pages/DataManagement/dataset.api"; import { mapDataset } from "@/pages/DataManagement/dataset.const"; import { App, Button, Form, Input, Modal, Select, Radio, Table } from "antd"; import TextArea from "antd/es/input/TextArea"; import { useEffect, useState } from "react"; import { Eye } from "lucide-react"; import { createAnnotationTaskUsingPost, getAnnotationTaskByIdUsingGet, updateAnnotationTaskByIdUsingPut, queryAnnotationTemplatesUsingGet, } from "../../annotation.api"; import { type Dataset } from "@/pages/DataManagement/dataset.model"; import type { AnnotationTemplate, AnnotationTask } from "../../annotation.model"; import LabelStudioEmbed from "@/components/business/LabelStudioEmbed"; import TemplateConfigurationForm from "../../components/TemplateConfigurationForm"; interface AnnotationTaskDialogProps { open: boolean; onClose: () => void; onRefresh: () => void; /** 编辑模式:传入要编辑的任务数据 */ editTask?: AnnotationTask | null; } export default function CreateAnnotationTask({ open, onClose, onRefresh, editTask, }: AnnotationTaskDialogProps) { const isEditMode = !!editTask; const { message } = App.useApp(); const [manualForm] = Form.useForm(); const [datasets, setDatasets] = useState([]); const [templates, setTemplates] = useState([]); const [submitting, setSubmitting] = useState(false); const [nameManuallyEdited, setNameManuallyEdited] = useState(false); // Custom template state const [customXml, setCustomXml] = useState(""); const [showPreview, setShowPreview] = useState(false); const [previewTaskData, setPreviewTaskData] = useState>({}); const [configMode, setConfigMode] = useState<"template" | "custom">("template"); // 模板编辑模式切换(可视化 vs XML) const [templateEditTab, setTemplateEditTab] = useState<"visual" | "xml">("visual"); // 是否已选择模板(用于启用受限编辑模式) const [hasSelectedTemplate, setHasSelectedTemplate] = useState(false); // 数据集预览相关状态 const [datasetPreviewVisible, setDatasetPreviewVisible] = useState(false); const [datasetPreviewData, setDatasetPreviewData] = useState([]); const [datasetPreviewLoading, setDatasetPreviewLoading] = useState(false); const [selectedDatasetId, setSelectedDatasetId] = useState(null); // 文件内容预览相关状态 const [fileContentVisible, setFileContentVisible] = useState(false); const [fileContent, setFileContent] = useState(""); const [fileContentLoading, setFileContentLoading] = useState(false); const [previewFileName, setPreviewFileName] = useState(""); const [previewFileType, setPreviewFileType] = useState<"text" | "image" | "video" | "audio">("text"); const [previewMediaUrl, setPreviewMediaUrl] = useState(""); // 任务详情加载状态(编辑模式) const [taskDetailLoading, setTaskDetailLoading] = useState(false); 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 || []; 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, or load task data in edit mode useEffect(() => { if (open) { manualForm.resetFields(); setNameManuallyEdited(false); setCustomXml(""); setShowPreview(false); setPreviewTaskData({}); setDatasetPreviewData([]); if (isEditMode && editTask) { // 编辑模式:加载任务详情 setTaskDetailLoading(true); getAnnotationTaskByIdUsingGet(editTask.id) .then((res: any) => { if (res.code === 200 && res.data) { const taskDetail = res.data; // 填充基本信息 manualForm.setFieldsValue({ name: taskDetail.name, description: taskDetail.description, datasetId: taskDetail.datasetId, }); setSelectedDatasetId(taskDetail.datasetId); // 填充模板配置 if (taskDetail.configuration) { const { objects, labels } = taskDetail.configuration; manualForm.setFieldsValue({ objects: objects || [], labels: labels || [], }); } // 设置 XML 配置用于预览 if (taskDetail.labelConfig) { setCustomXml(taskDetail.labelConfig); } // 编辑模式始终使用 custom 配置模式(不改变结构,只改标签) setConfigMode("custom"); // 编辑模式下启用受限编辑 setHasSelectedTemplate(true); } }) .catch((err) => { console.error("Failed to load task detail:", err); message.error("加载任务详情失败"); }) .finally(() => { setTaskDetailLoading(false); }); } else { // 创建模式:重置为默认状态 setConfigMode("template"); setHasSelectedTemplate(false); setSelectedDatasetId(null); } } }, [open, manualForm, isEditMode, editTask, message]); // 预览数据集 const handlePreviewDataset = async () => { if (!selectedDatasetId) { message.warning("请先选择数据集"); return; } setDatasetPreviewLoading(true); try { const res = await queryDatasetFilesUsingGet(selectedDatasetId, { page: 0, size: 10 }); if (res.code === '0' && res.data) { setDatasetPreviewData(res.data.content || []); setDatasetPreviewVisible(true); } else { message.error("获取数据集预览失败"); } } catch (error) { console.error("Preview dataset error:", error); message.error("获取数据集预览失败"); } finally { setDatasetPreviewLoading(false); } }; // 预览文件内容 const handlePreviewFileContent = async (file: any) => { const fileName = file.fileName?.toLowerCase() || ''; // 文件类型扩展名映射 const textExtensions = ['.json', '.jsonl', '.txt', '.csv', '.tsv', '.xml', '.md', '.yaml', '.yml']; const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']; const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi']; const audioExtensions = ['.mp3', '.wav', '.ogg', '.aac', '.flac', '.m4a']; const isTextFile = textExtensions.some(ext => fileName.endsWith(ext)); const isImageFile = imageExtensions.some(ext => fileName.endsWith(ext)); const isVideoFile = videoExtensions.some(ext => fileName.endsWith(ext)); const isAudioFile = audioExtensions.some(ext => fileName.endsWith(ext)); if (!isTextFile && !isImageFile && !isVideoFile && !isAudioFile) { message.warning("不支持预览该文件类型"); return; } setFileContentLoading(true); setPreviewFileName(file.fileName); const fileUrl = `/api/data-management/datasets/${selectedDatasetId}/files/${file.id}/download`; try { if (isTextFile) { // 文本文件:获取内容 const response = await fetch(fileUrl); if (!response.ok) { throw new Error('下载失败'); } const text = await response.text(); // 限制预览内容长度 const maxLength = 50000; if (text.length > maxLength) { setFileContent(text.substring(0, maxLength) + '\n\n... (内容过长,仅显示前 50000 字符)'); } else { setFileContent(text); } setPreviewFileType("text"); } else if (isImageFile) { // 图片文件:直接使用 URL setPreviewMediaUrl(fileUrl); setPreviewFileType("image"); } else if (isVideoFile) { // 视频文件:使用 URL setPreviewMediaUrl(fileUrl); setPreviewFileType("video"); } else if (isAudioFile) { // 音频文件:使用 URL setPreviewMediaUrl(fileUrl); setPreviewFileType("audio"); } setFileContentVisible(true); } catch (error) { console.error("Preview file content error:", error); message.error("获取文件内容失败"); } finally { setFileContentLoading(false); } }; 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; }; // 根据 objects 配置生成预览用的示例数据 const generateExampleData = (objects: any[]) => { const exampleUrls: Record = { Image: "https://labelstud.io/images/opa-header.png", Audio: "https://labelstud.io/files/sample.wav", Video: "https://labelstud.io/files/sample.mp4", }; const exampleTexts: Record = { Text: "这是示例文本,用于预览标注界面。", HyperText: "

这是示例 HTML 内容

", Header: "示例标题", Paragraphs: "段落一\n\n段落二\n\n段落三", }; const data: Record = {}; if (!objects || objects.length === 0) { // 默认数据 return { image: exampleUrls.Image, text: exampleTexts.Text, audio: exampleUrls.Audio, }; } objects.forEach((obj: any) => { if (!obj?.name || !obj?.value) return; // 变量名从 $varName 中提取 const varName = obj.value.startsWith("$") ? obj.value.slice(1) : obj.name; if (exampleUrls[obj.type]) { data[varName] = exampleUrls[obj.type]; } else if (exampleTexts[obj.type]) { data[varName] = exampleTexts[obj.type]; } else { // 未知类型,尝试根据名称猜测 const lowerName = varName.toLowerCase(); if (lowerName.includes("image") || lowerName.includes("img")) { data[varName] = exampleUrls.Image; } else if (lowerName.includes("audio") || lowerName.includes("sound")) { data[varName] = exampleUrls.Audio; } else if (lowerName.includes("video")) { data[varName] = exampleUrls.Video; } else { data[varName] = exampleTexts.Text; } } }); return data; }; // 当选择模板时,加载模板配置到表单 const handleTemplateSelect = (value: string, option: any) => { // 处理清除选择的情况 if (!value) { setHasSelectedTemplate(false); setCustomXml(""); return; } setHasSelectedTemplate(true); 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, // 编辑模式需要传递配置结构,用于后端保存 configuration: { objects: objects || [], labels: labels || [], }, }; if (isEditMode && editTask) { // 编辑模式:调用更新接口 await updateAnnotationTaskByIdUsingPut(editTask.id, requestData); message.success("更新标注任务成功"); } else { // 创建模式:调用创建接口 await createAnnotationTaskUsingPost(requestData); message.success("创建标注任务成功"); } onClose(); onRefresh(); } catch (err: any) { console.error(isEditMode ? "Update annotation task failed" : "Create annotation task failed", err); const msg = err?.message || err?.data?.message || (isEditMode ? "更新失败,请稍后重试" : "创建失败,请稍后重试"); message.error(msg); } finally { setSubmitting(false); } }; 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} >
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
数据集
} name="datasetId" rules={[{ required: true, message: "请选择数据集" }]} help={isEditMode ? "数据集不可修改" : undefined} > setNameManuallyEdited(true)} /> {/* 描述变为可选 */}