diff --git a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx index 3fa53a5..afaf393 100644 --- a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx +++ b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx @@ -824,7 +824,7 @@ export default function LabelStudioTextEditor() { /> {segmented && ( -
+
段落/分段 diff --git a/frontend/src/pages/DataAnnotation/Create/CreateTask.tsx b/frontend/src/pages/DataAnnotation/Create/CreateTask.tsx index ac89d4a..fa67091 100644 --- a/frontend/src/pages/DataAnnotation/Create/CreateTask.tsx +++ b/frontend/src/pages/DataAnnotation/Create/CreateTask.tsx @@ -1,12 +1,13 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Button, Input, Select, Form, message, Radio } from "antd"; +import type { RadioChangeEvent } from "antd"; import TextArea from "antd/es/input/TextArea"; import { DatabaseOutlined } from "@ant-design/icons"; import { Link, useNavigate } from "react-router"; import { ArrowLeft } from "lucide-react"; import { queryDatasetsUsingGet } from "../../DataManagement/dataset.api"; import { mapDataset } from "@/pages/DataManagement/dataset.const"; -import type { Dataset } from "@/pages/DataManagement/dataset.model"; +import { DatasetType, type Dataset } from "@/pages/DataManagement/dataset.model"; import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet, @@ -14,20 +15,33 @@ import { import type { AnnotationTemplate } from "../annotation.model"; import TemplateConfigurationTreeEditor from "../components/TemplateConfigurationTreeEditor"; +const DEFAULT_SEGMENTATION_ENABLED = true; +const SEGMENTATION_OPTIONS = [ + { label: "需要切片段", value: true }, + { label: "不需要切片段", value: false }, +]; + export default function AnnotationTaskCreate() { const navigate = useNavigate(); const [form] = Form.useForm(); const [datasets, setDatasets] = useState([]); const [templates, setTemplates] = useState([]); + const [selectedDatasetId, setSelectedDatasetId] = useState(null); const [labelConfig, setLabelConfig] = useState(""); const [configMode, setConfigMode] = useState<"template" | "custom">("template"); const [submitting, setSubmitting] = useState(false); + const selectedDataset = useMemo( + () => datasets.find((dataset) => dataset.id === selectedDatasetId), + [datasets, selectedDatasetId] + ); + const isTextDataset = selectedDataset?.datasetType === DatasetType.TEXT; + const fetchDatasets = async () => { try { const { data } = await queryDatasetsUsingGet({ page: 0, pageSize: 1000 }); const list = data?.content || []; - setDatasets(list.map((item: any) => mapDataset(item)) || []); + setDatasets(list.map((item) => mapDataset(item)) || []); } catch (error) { console.error("加载数据集失败:", error); message.error("加载数据集失败"); @@ -62,7 +76,7 @@ export default function AnnotationTaskCreate() { setLabelConfig(selectedTemplate?.labelConfig || ""); }; - const handleConfigModeChange = (e: any) => { + const handleConfigModeChange = (e: RadioChangeEvent) => { const mode = e.target.value; setConfigMode(mode); if (mode === "custom") { @@ -79,20 +93,26 @@ export default function AnnotationTaskCreate() { } setSubmitting(true); - await createAnnotationTaskUsingPost({ + const requestData: Record = { name: values.name, description: values.description, datasetId: values.datasetId, templateId: configMode === "template" ? values.templateId : undefined, labelConfig: labelConfig.trim(), - }); + }; + if (isTextDataset) { + requestData.segmentationEnabled = + values.segmentationEnabled ?? DEFAULT_SEGMENTATION_ENABLED; + } + await createAnnotationTaskUsingPost(requestData); message.success("标注任务创建成功"); navigate("/data/annotation"); - } catch (error: any) { - if (error?.errorFields) { + } catch (error: unknown) { + const err = error as { errorFields?: unknown; message?: string; data?: { message?: string } }; + if (err?.errorFields) { message.error("请完善必填信息"); } else { - const msg = error?.message || error?.data?.message || "创建失败,请稍后重试"; + const msg = err?.message || err?.data?.message || "创建失败,请稍后重试"; message.error(msg); console.error(error); } @@ -149,6 +169,40 @@ export default function AnnotationTaskCreate() { ), value: dataset.id, }))} + onChange={(value) => { + setSelectedDatasetId(value); + const dataset = datasets.find((item) => item.id === value); + if (dataset?.datasetType === DatasetType.TEXT) { + const currentValue = form.getFieldValue("segmentationEnabled"); + if (currentValue === undefined) { + form.setFieldsValue({ + segmentationEnabled: DEFAULT_SEGMENTATION_ENABLED, + }); + } + } else if (dataset) { + form.setFieldsValue({ segmentationEnabled: false }); + } + }} + /> + + + + diff --git a/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx b/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx index f45c92b..7d0efc1 100644 --- a/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx +++ b/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx @@ -1,8 +1,10 @@ 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 type { RadioChangeEvent } from "antd"; import TextArea from "antd/es/input/TextArea"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import type { ReactNode } from "react"; import { Eye } from "lucide-react"; import { createAnnotationTaskUsingPost, @@ -10,7 +12,7 @@ import { updateAnnotationTaskByIdUsingPut, queryAnnotationTemplatesUsingGet, } from "../../annotation.api"; -import { type Dataset } from "@/pages/DataManagement/dataset.model"; +import { DatasetType, type Dataset } from "@/pages/DataManagement/dataset.model"; import type { AnnotationTemplate, AnnotationTask } from "../../annotation.model"; import LabelStudioEmbed from "@/components/business/LabelStudioEmbed"; import TemplateConfigurationTreeEditor from "../../components/TemplateConfigurationTreeEditor"; @@ -24,6 +26,38 @@ interface AnnotationTaskDialogProps { editTask?: AnnotationTask | null; } +type DatasetOption = Dataset & { icon?: ReactNode }; + +type DatasetPreviewFile = { + id: string; + fileName?: string; + fileSize?: number; +}; + +type AnnotationTaskDetail = { + name?: string; + description?: string; + datasetId?: string; + labelConfig?: string; + template?: { labelConfig?: string }; + segmentationEnabled?: boolean; +}; + +type ApiResponse = { + code?: number; + message?: string; + data?: T; +}; + +const isRecord = (value: unknown): value is Record => + !!value && typeof value === "object" && !Array.isArray(value); + +const DEFAULT_SEGMENTATION_ENABLED = true; +const SEGMENTATION_OPTIONS = [ + { label: "需要切片段", value: true }, + { label: "不需要切片段", value: false }, +]; + export default function CreateAnnotationTask({ open, onClose, @@ -33,19 +67,19 @@ export default function CreateAnnotationTask({ const isEditMode = !!editTask; const { message } = App.useApp(); const [manualForm] = Form.useForm(); - const [datasets, setDatasets] = useState([]); + const [datasets, setDatasets] = useState([]); const [templates, setTemplates] = useState([]); const [submitting, setSubmitting] = useState(false); const [nameManuallyEdited, setNameManuallyEdited] = useState(false); const [labelConfig, setLabelConfig] = useState(""); const [showPreview, setShowPreview] = useState(false); - const [previewTaskData, setPreviewTaskData] = useState>({}); + const [previewTaskData, setPreviewTaskData] = useState>({}); const [configMode, setConfigMode] = useState<"template" | "custom">("template"); // 数据集预览相关状态 const [datasetPreviewVisible, setDatasetPreviewVisible] = useState(false); - const [datasetPreviewData, setDatasetPreviewData] = useState([]); + const [datasetPreviewData, setDatasetPreviewData] = useState([]); const [datasetPreviewLoading, setDatasetPreviewLoading] = useState(false); const [selectedDatasetId, setSelectedDatasetId] = useState(null); @@ -61,6 +95,12 @@ export default function CreateAnnotationTask({ const [taskDetailLoading, setTaskDetailLoading] = useState(false); const { config: tagConfig } = useTagConfig(false); + const selectedDataset = useMemo( + () => datasets.find((dataset) => dataset.id === selectedDatasetId), + [datasets, selectedDatasetId] + ); + const isTextDataset = selectedDataset?.datasetType === DatasetType.TEXT; + useEffect(() => { if (!open) return; const fetchData = async () => { @@ -107,7 +147,7 @@ export default function CreateAnnotationTask({ // 编辑模式:加载任务详情 setTaskDetailLoading(true); getAnnotationTaskByIdUsingGet(editTask.id) - .then((res: any) => { + .then((res: ApiResponse) => { if (res.code === 200 && res.data) { const taskDetail = res.data; // 填充基本信息 @@ -115,8 +155,13 @@ export default function CreateAnnotationTask({ name: taskDetail.name, description: taskDetail.description, datasetId: taskDetail.datasetId, + segmentationEnabled: typeof taskDetail.segmentationEnabled === "boolean" + ? taskDetail.segmentationEnabled + : DEFAULT_SEGMENTATION_ENABLED, }); - setSelectedDatasetId(taskDetail.datasetId); + if (taskDetail.datasetId) { + setSelectedDatasetId(taskDetail.datasetId); + } // 获取实际的 labelConfig(优先使用任务自身的配置,回退到模板配置) const configXml = taskDetail.labelConfig || taskDetail.template?.labelConfig; @@ -140,6 +185,9 @@ export default function CreateAnnotationTask({ // 创建模式:重置为默认状态 setConfigMode("template"); setSelectedDatasetId(null); + manualForm.setFieldsValue({ + segmentationEnabled: DEFAULT_SEGMENTATION_ENABLED, + }); } } }, [open, manualForm, isEditMode, editTask, message]); @@ -154,7 +202,7 @@ export default function CreateAnnotationTask({ try { const res = await queryDatasetFilesUsingGet(selectedDatasetId, { page: 0, size: 10 }); if (res.code === '0' && res.data) { - setDatasetPreviewData(res.data.content || []); + setDatasetPreviewData((res.data.content || []) as DatasetPreviewFile[]); setDatasetPreviewVisible(true); } else { message.error("获取数据集预览失败"); @@ -168,7 +216,7 @@ export default function CreateAnnotationTask({ }; // 预览文件内容 - const handlePreviewFileContent = async (file: any) => { + const handlePreviewFileContent = async (file: DatasetPreviewFile) => { const fileName = file.fileName?.toLowerCase() || ''; // 文件类型扩展名映射 @@ -318,7 +366,7 @@ export default function CreateAnnotationTask({ }; const generatePreviewTaskDataFromLabelConfig = (xml: string) => { - const exampleDataByType: Record = { + const exampleDataByType: Record = { Image: "https://labelstud.io/images/opa-header.png", Audio: "https://labelstud.io/files/sample.wav", AudioPlus: "https://labelstud.io/files/sample.wav", @@ -350,7 +398,7 @@ export default function CreateAnnotationTask({ }; } - const data: Record = {}; + const data: Record = {}; objects.forEach((obj) => { const name = obj.name || ""; const value = obj.value || ""; @@ -380,14 +428,16 @@ export default function CreateAnnotationTask({ }; // 当选择模板时,加载 XML 配置到树编辑器(仅快速填充) - const handleTemplateSelect = (value: string, option: any) => { + const handleTemplateSelect = (value: string, option: unknown) => { if (!value) { setLabelConfig(""); return; } const selectedTemplate = templates.find((template) => template.id === value); - const configXml = selectedTemplate?.labelConfig || option?.config || ""; + const configXml = selectedTemplate?.labelConfig + || (isRecord(option) && typeof option.config === "string" ? option.config : "") + || ""; setLabelConfig(configXml); }; @@ -437,6 +487,10 @@ export default function CreateAnnotationTask({ templateId: configMode === "template" ? values.templateId : undefined, labelConfig: labelConfig.trim(), }; + if (!isEditMode && isTextDataset) { + requestData.segmentationEnabled = + values.segmentationEnabled ?? DEFAULT_SEGMENTATION_ENABLED; + } if (isEditMode && editTask) { // 编辑模式:调用更新接口 @@ -449,16 +503,17 @@ export default function CreateAnnotationTask({ } onClose(); onRefresh(); - } catch (err: any) { + } catch (err: unknown) { console.error(isEditMode ? "Update annotation task failed" : "Create annotation task failed", err); - const msg = err?.message || err?.data?.message || (isEditMode ? "更新失败,请稍后重试" : "创建失败,请稍后重试"); + const error = err as { message?: string; data?: { message?: string } }; + const msg = error?.message || error?.data?.message || (isEditMode ? "更新失败,请稍后重试" : "创建失败,请稍后重试"); message.error(msg); } finally { setSubmitting(false); } }; - const handleConfigModeChange = (e: any) => { + const handleConfigModeChange = (e: RadioChangeEvent) => { const mode = e.target.value; setConfigMode(mode); if (mode === "custom") { @@ -521,7 +576,7 @@ export default function CreateAnnotationTask({ label: (
- {(dataset as any).icon} + {dataset.icon} {dataset.name}
{dataset.size}
@@ -532,6 +587,17 @@ export default function CreateAnnotationTask({ })} onChange={(value) => { setSelectedDatasetId(value); + const dataset = datasets.find((item) => item.id === value); + if (dataset?.datasetType === DatasetType.TEXT) { + const currentValue = manualForm.getFieldValue("segmentationEnabled"); + if (currentValue === undefined) { + manualForm.setFieldsValue({ + segmentationEnabled: DEFAULT_SEGMENTATION_ENABLED, + }); + } + } else if (dataset) { + manualForm.setFieldsValue({ segmentationEnabled: false }); + } // 如果用户未手动修改名称,则用数据集名称作为默认任务名 if (!nameManuallyEdited) { const ds = datasets.find((d) => d.id === value); @@ -578,6 +644,28 @@ export default function CreateAnnotationTask({