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, queryAnnotationTemplatesUsingGet, } from "../../annotation.api"; import { type Dataset } from "@/pages/DataManagement/dataset.model"; import type { AnnotationTemplate } from "../../annotation.model"; import LabelStudioEmbed from "@/components/business/LabelStudioEmbed"; import TemplateConfigurationForm from "../../components/TemplateConfigurationForm"; export default function CreateAnnotationTask({ open, onClose, onRefresh, }: { open: boolean; onClose: () => void; onRefresh: () => void; }) { 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"); // 是否已选择模板(用于启用受限编辑模式) 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(""); 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 useEffect(() => { if (open) { manualForm.resetFields(); setNameManuallyEdited(false); setCustomXml(""); setShowPreview(false); setPreviewTaskData({}); setConfigMode("template"); setHasSelectedTemplate(false); setSelectedDatasetId(null); setDatasetPreviewData([]); } }, [open, manualForm]); // 预览数据集 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 textExtensions = ['.json', '.jsonl', '.txt', '.csv', '.tsv', '.xml', '.md', '.yaml', '.yml']; const fileName = file.fileName?.toLowerCase() || ''; const isTextFile = textExtensions.some(ext => fileName.endsWith(ext)); if (!isTextFile) { message.warning("仅支持预览文本类文件(JSON、JSONL、TXT、CSV 等)"); return; } setFileContentLoading(true); setPreviewFileName(file.fileName); try { const response = await fetch(`/api/data-management/datasets/${selectedDatasetId}/files/${file.id}/download`); 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); } 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, }; 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.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: "请选择数据集" }]} > setNameManuallyEdited(true)} /> {/* 描述变为可选 */}