diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json
index 62bf982ce..4c23c8de4 100644
--- a/frontend/src/i18n/locales/en/common.json
+++ b/frontend/src/i18n/locales/en/common.json
@@ -580,6 +580,9 @@
"actions": {
"createTask": "Create Task"
},
+ "table": {
+ "file": "files"
+ },
"placeholders": {
"empty": "-"
},
@@ -964,5 +967,309 @@
"score": "Score:",
"scoreSuffix": " pts"
}
+ },
+ "dataAnnotation": {
+ "home": {
+ "title": "Data Annotation",
+ "tabs": {
+ "tasks": "Annotation Tasks",
+ "templates": "Annotation Templates"
+ },
+ "columns": {
+ "taskName": "Task Name",
+ "type": "Type",
+ "taskId": "Task ID",
+ "dataset": "Dataset",
+ "model": "Model",
+ "confidence": "Confidence",
+ "targetClasses": "Target Classes",
+ "autoStatus": "Auto Status",
+ "autoProgress": "Auto Progress",
+ "detectedObjects": "Detected Objects",
+ "createdAt": "Created At",
+ "updatedAt": "Updated At",
+ "actions": "Actions"
+ },
+ "actions": {
+ "annotate": "Annotate",
+ "edit": "Edit",
+ "syncToDb": "Sync to Database",
+ "exportResult": "Export Annotations",
+ "delete": "Delete",
+ "moreActions": "More Actions"
+ },
+ "confirm": {
+ "deleteTaskTitle": "Confirm delete annotation task '{name}'?",
+ "deleteTaskContent1": "Deleting the annotation task will not delete the corresponding dataset.",
+ "deleteTaskContent2": "If you want to keep current annotation results, please sync before deleting.",
+ "deleteAutoTaskTitle": "Confirm delete auto annotation task '{name}'?",
+ "deleteAutoTaskContent": "After deleting the task, the generated annotation results will not be deleted.",
+ "deleteOkText": "Delete",
+ "deleteCancelText": "Cancel",
+ "syncTitle": "Confirm sync annotation task '{name}'?",
+ "syncContent1": "The file list in the annotation project will be consistent with the dataset, and differences will be corrected.",
+ "syncContent2": "Labels in the annotation project will be merged with labels in the dataset, conflicts will be based on the latest content.",
+ "syncOkText": "Sync",
+ "syncCancelText": "Cancel",
+ "batchSyncTitle": "Confirm sync {count} selected annotation tasks?",
+ "batchDeleteTitle": "Confirm delete {count} selected annotation tasks?"
+ },
+ "messages": {
+ "deleteSuccess": "Mapping deleted successfully",
+ "deleteFailed": "Deletion failed, please try again later",
+ "autoTaskDeleteSuccess": "Auto annotation task deleted successfully",
+ "batchSyncSuccess": "Batch sync request sent",
+ "batchSyncFailed": "Batch sync failed, please try again later",
+ "batchDeleteSuccess": "Batch delete completed",
+ "batchDeleteFailed": "Batch delete failed, please try again later",
+ "syncRequestSent": "Task sync request sent",
+ "syncFailed": "Sync failed, please try again later",
+ "cannotJumpNoBase": "Cannot jump to Label Studio: Label Studio base URL not configured",
+ "cannotJumpNoMapping": "Cannot jump to Label Studio: No mapping bound to annotation project",
+ "cannotJumpError": "Cannot jump to Label Studio: Error occurred, please check configuration or console logs",
+ "cannotJumpAutoNoDataset": "This auto annotation task is not bound to a dataset, cannot jump to Label Studio",
+ "cannotJumpAutoNoProject": "No corresponding annotation project found, please create annotation project for this task or dataset first",
+ "autoTaskNotFound": "Corresponding auto annotation task not found",
+ "taskNotFound": "Corresponding annotation task not found",
+ "fetchAutoTasksFailed": "Failed to fetch auto annotation tasks",
+ "syncToDbTitle": "Confirm sync '{name}' annotations from Label Studio to database?",
+ "syncToDbContent1": "This operation will override current file tags and annotation information based on task data in Label Studio.",
+ "syncToDbContent2": "After sync, you can view latest tags and annotations in dataset file details.",
+ "syncToDbLoading": "Syncing annotations from Label Studio to database...",
+ "syncToDbSuccess": "Sync completed",
+ "syncToDbFailed": "Sync failed, please try again later",
+ "batchSyncManualOnly": "Please select manual annotation tasks for sync"
+ },
+ "searchPlaceholder": "Search task name, description",
+ "autoStatusLabels": {
+ "pending": "Pending",
+ "running": "Running",
+ "completed": "Completed",
+ "failed": "Failed",
+ "cancelled": "Cancelled"
+ },
+ "autoModelSizeLabels": {
+ "n": "YOLOv8n (Fastest)",
+ "s": "YOLOv8s",
+ "m": "YOLOv8m",
+ "l": "YOLOv8l (Recommended)",
+ "x": "YOLOv8x (Most Accurate)"
+ },
+ "batchSync": "Batch Sync",
+ "batchDelete": "Batch Delete",
+ "createTask": "Create Annotation Task",
+ "manualAnnotation": "Manual Annotation",
+ "autoAnnotation": "Auto Annotation",
+ "allCategories": "All Categories",
+ "categoriesCount": "{count} categories",
+ "editDataset": "Edit Dataset",
+ "editTask": "Edit Task"
+ },
+ "create": {
+ "title": "Create Annotation Task",
+ "back": "Back",
+ "cancel": "Cancel",
+ "submit": "Create Task",
+ "basicInfo": "Basic Information",
+ "form": {
+ "name": "Task Name",
+ "namePlaceholder": "Enter task name",
+ "nameRequired": "Please enter task name",
+ "nameMinLength": "Task name must be at least 3 characters (excluding leading/trailing spaces, Label Studio limit)",
+ "nameMaxLength": "Task name cannot exceed 100 characters",
+ "description": "Task Description",
+ "descriptionPlaceholder": "Describe the requirements and goals of the annotation task in detail",
+ "dataset": "Select Dataset",
+ "datasetRequired": "Please select dataset",
+ "template": "Annotation Template",
+ "templateRequired": "Please select annotation template",
+ "noTemplatesAvailable": "No templates available, please create templates first",
+ "selectTemplate": "Please select annotation template",
+ "noTemplatesFound": "No matching templates found, please create templates in 'Annotation Templates' page",
+ "selectDatasetAndFiles": "Select Dataset and Files",
+ "currentDataset": "Current dataset: {name} - Selected {count} files",
+ "currentDatasetImages": "Current dataset: {name} - Selected {count} image files",
+ "modelSize": "Model Size",
+ "modelSizeRequired": "Please select model size",
+ "confThreshold": "Confidence Threshold",
+ "confThresholdRequired": "Please select confidence threshold",
+ "targetClasses": "Target Classes",
+ "selectAllClasses": "Select All Classes",
+ "selectTargetClasses": "Select target classes"
+ },
+ "templateCategories": {
+ "cv": "Computer Vision",
+ "nlp": "Natural Language Processing"
+ },
+ "customTemplate": "Custom Template",
+ "customTemplateDesc": "Create annotation template that meets specific needs",
+ "selectedTemplate": "Selected Template",
+ "templateSelection": "Template Selection",
+ "manual": "Manual Annotation",
+ "auto": "Auto Annotation",
+ "ok": "Confirm",
+ "messages": {
+ "selectAtLeastOneFile": "Please select at least one file",
+ "selectAtLeastOneImageFile": "Please select at least one image file",
+ "createSuccess": "Annotation task created successfully",
+ "createFailed": "Failed to create annotation task, please try again",
+ "autoCreateSuccess": "Auto annotation task created successfully",
+ "autoCreateFailed": "Failed to create auto annotation task",
+ "datasetTypeFiltered": "Datasets have been filtered by template type, please re-select dataset and files"
+ }
+ },
+ "template": {
+ "title": "Annotation Templates",
+ "create": "Create Template",
+ "filters": {
+ "category": "Classification",
+ "dataType": "Data Type",
+ "labelingType": "Labeling Type",
+ "builtIn": "Template Type"
+ },
+ "columns": {
+ "name": "Template Name",
+ "description": "Description",
+ "dataType": "Data Type",
+ "labelingType": "Labeling Type",
+ "category": "Classification",
+ "builtIn": "Type",
+ "version": "Version",
+ "createdAt": "Created At",
+ "actions": "Actions"
+ },
+ "messages": {
+ "deleteSuccess": "Template deleted successfully",
+ "deleteFailed": "Failed to delete template",
+ "deleteConfirm": "Are you sure you want to delete this template?",
+ "confirmDelete": "Confirm",
+ "cancelDelete": "Cancel"
+ },
+ "actions": {
+ "viewDetail": "View Details",
+ "edit": "Edit",
+ "delete": "Delete"
+ },
+ "searchPlaceholder": "Search template name, description"
+ },
+ "autoAnnotation": {
+ "title": "Auto Annotation",
+ "createTask": "Create Auto Annotation Task",
+ "columns": {
+ "name": "Task Name",
+ "dataset": "Dataset",
+ "modelSize": "Model Size",
+ "confThreshold": "Confidence",
+ "targetClasses": "Target Classes",
+ "status": "Status",
+ "progress": "Progress",
+ "detectedObjects": "Detected Objects",
+ "createdAt": "Created At",
+ "actions": "Actions"
+ },
+ "statusLabels": {
+ "pending": "Pending",
+ "running": "Running",
+ "completed": "Completed",
+ "failed": "Failed",
+ "cancelled": "Cancelled"
+ },
+ "modelSizeLabels": {
+ "n": "YOLOv8n (Fastest)",
+ "s": "YOLOv8s",
+ "m": "YOLOv8m",
+ "l": "YOLOv8l (Recommended)",
+ "x": "YOLOv8x (Most Accurate)"
+ },
+ "actions": {
+ "annotate": "Annotate",
+ "syncToDb": "Sync to Database",
+ "exportResult": "Export Annotations",
+ "editDataset": "Edit Dataset",
+ "delete": "Delete",
+ "moreActions": "More Actions"
+ },
+ "confirm": {
+ "deleteTitle": "Confirm delete auto annotation task '{name}'?",
+ "deleteContent": "After deleting the task, the generated annotation results will not be deleted.",
+ "deleteOkText": "Delete",
+ "deleteCancelText": "Cancel"
+ },
+ "messages": {
+ "deleteSuccess": "Auto annotation task deleted successfully",
+ "deleteFailed": "Deletion failed, please try again later"
+ }
+ },
+ "annotate": {
+ "title": "Annotation Workspace",
+ "saveSuccess": "Annotation saved",
+ "skipSuccess": "Skipped",
+ "backToList": "Back to List"
+ },
+ "dialogs": {
+ "editDataset": {
+ "title": "Edit Dataset",
+ "description": "Modify dataset files for this task",
+ "selectedCount": "Selected {count} files",
+ "cancel": "Cancel",
+ "save": "Save",
+ "loading": "Saving...",
+ "success": "Dataset updated successfully",
+ "failed": "Failed to update dataset, please try again later",
+ "fetchFilesFailed": "Failed to fetch task current dataset files"
+ },
+ "importFromLS": {
+ "title": "Import from Label Studio",
+ "description": "Import annotation results from Label Studio to local",
+ "cancel": "Cancel",
+ "import": "Import",
+ "loading": "Importing...",
+ "success": "Import successful",
+ "failed": "Import failed, please try again later"
+ },
+ "syncToDb": {
+ "loading": "Syncing annotations from Label Studio to database...",
+ "success": "Sync completed",
+ "fail": "Sync failed, please try again later"
+ }
+ },
+ "const": {
+ "status": {
+ "active": "Active",
+ "processing": "Processing",
+ "inactive": "Inactive",
+ "completed": "Completed",
+ "skipped": "Skipped",
+ "pending": "Ready"
+ },
+ "dataType": {
+ "text": "Text",
+ "image": "Image",
+ "audio": "Audio",
+ "video": "Video"
+ },
+ "classification": {
+ "cv": "Computer Vision",
+ "nlp": "Natural Language Processing",
+ "audio": "Audio",
+ "qualityControl": "Quality Control",
+ "custom": "Custom"
+ },
+ "annotationType": {
+ "classification": "Classification",
+ "objectDetection": "Object Detection",
+ "segmentation": "Segmentation",
+ "ner": "Named Entity Recognition"
+ },
+ "templateType": {
+ "system": "System Built-in",
+ "custom": "Custom"
+ },
+ "stats": {
+ "accuracy": "Accuracy",
+ "averageTime": "Average Time",
+ "reviewCount": "Pending Review"
+ }
+ }
}
}
diff --git a/frontend/src/i18n/locales/zh/common.json b/frontend/src/i18n/locales/zh/common.json
index c0b1e30da..8daee35ce 100644
--- a/frontend/src/i18n/locales/zh/common.json
+++ b/frontend/src/i18n/locales/zh/common.json
@@ -963,5 +963,309 @@
"score": "评分:",
"scoreSuffix": "分"
}
+ },
+ "dataAnnotation": {
+ "home": {
+ "title": "数据标注",
+ "tabs": {
+ "tasks": "标注任务",
+ "templates": "标注模板"
+ },
+ "columns": {
+ "taskName": "任务名称",
+ "type": "类型",
+ "taskId": "任务ID",
+ "dataset": "数据集",
+ "model": "模型",
+ "confidence": "置信度",
+ "targetClasses": "目标类别",
+ "autoStatus": "自动标注状态",
+ "autoProgress": "自动标注进度",
+ "detectedObjects": "检测对象数",
+ "createdAt": "创建时间",
+ "updatedAt": "更新时间",
+ "actions": "操作"
+ },
+ "actions": {
+ "annotate": "标注",
+ "edit": "编辑",
+ "syncToDb": "同步到数据库",
+ "exportResult": "导出标注结果",
+ "delete": "删除",
+ "moreActions": "更多操作"
+ },
+ "confirm": {
+ "deleteTaskTitle": "确认删除标注任务「{name}」吗?",
+ "deleteTaskContent1": "删除标注任务不会删除对应数据集。",
+ "deleteTaskContent2": "如需保留当前标注结果,请在同步后再删除。",
+ "deleteAutoTaskTitle": "确认删除自动标注任务「{name}」吗?",
+ "deleteAutoTaskContent": "删除任务后,已生成的标注结果不会被删除。",
+ "deleteOkText": "删除",
+ "deleteCancelText": "取消",
+ "syncTitle": "确认同步标注任务「{name}」吗?",
+ "syncContent1": "标注工程中文件列表将与数据集保持一致,差异项将会被修正。",
+ "syncContent2": "标注工程中的标签与数据集中标签将进行合并,冲突项将以最新一次内容为准。",
+ "syncOkText": "同步",
+ "syncCancelText": "取消",
+ "batchSyncTitle": "确认同步所选 {count} 个标注任务吗?",
+ "batchDeleteTitle": "确认删除所选 {count} 个标注任务吗?"
+ },
+ "messages": {
+ "deleteSuccess": "映射删除成功",
+ "deleteFailed": "删除失败,请稍后重试",
+ "autoTaskDeleteSuccess": "自动标注任务删除成功",
+ "batchSyncSuccess": "批量同步请求已发送",
+ "batchSyncFailed": "批量同步失败,请稍后重试",
+ "batchDeleteSuccess": "批量删除已完成",
+ "batchDeleteFailed": "批量删除失败,请稍后重试",
+ "syncRequestSent": "任务同步请求已发送",
+ "syncFailed": "同步失败,请稍后重试",
+ "cannotJumpNoBase": "无法跳转到 Label Studio:未配置 Label Studio 基础 URL",
+ "cannotJumpNoMapping": "无法跳转到 Label Studio:该映射未绑定标注项目",
+ "cannotJumpError": "无法跳转到 Label Studio:发生错误,请检查配置或控制台日志",
+ "cannotJumpAutoNoDataset": "该自动标注任务未绑定数据集,无法跳转 Label Studio",
+ "cannotJumpAutoNoProject": "未找到对应的标注工程,请先为该任务或数据集创建标注项目",
+ "autoTaskNotFound": "未找到对应的自动标注任务",
+ "taskNotFound": "未找到对应的标注任务",
+ "fetchAutoTasksFailed": "获取自动标注任务失败",
+ "syncToDbTitle": "确认将「{name}」在 Label Studio 中的标注结果同步到数据库吗?",
+ "syncToDbContent1": "此操作会根据 Label Studio 中的任务数据覆盖当前文件标签与标注信息。",
+ "syncToDbContent2": "同步完成后,可在数据管理的文件详情中查看最新标签与标注。",
+ "syncToDbLoading": "正在从 Label Studio 同步标注到数据库...",
+ "syncToDbSuccess": "同步完成",
+ "syncToDbFailed": "同步失败,请稍后重试",
+ "batchSyncManualOnly": "请选择手动标注任务进行同步"
+ },
+ "searchPlaceholder": "搜索任务名称、描述",
+ "autoStatusLabels": {
+ "pending": "等待中",
+ "running": "处理中",
+ "completed": "已完成",
+ "failed": "失败",
+ "cancelled": "已取消"
+ },
+ "autoModelSizeLabels": {
+ "n": "YOLOv8n (最快)",
+ "s": "YOLOv8s",
+ "m": "YOLOv8m",
+ "l": "YOLOv8l (推荐)",
+ "x": "YOLOv8x (最精确)"
+ },
+ "batchSync": "批量同步",
+ "batchDelete": "批量删除",
+ "createTask": "创建标注任务",
+ "manualAnnotation": "手动标注",
+ "autoAnnotation": "自动标注",
+ "allCategories": "全部类别",
+ "categoriesCount": "{count} 个类别",
+ "editDataset": "编辑任务数据集",
+ "editTask": "编辑任务"
+ },
+ "create": {
+ "title": "创建标注任务",
+ "back": "返回",
+ "cancel": "取消",
+ "submit": "创建任务",
+ "basicInfo": "基本信息",
+ "form": {
+ "name": "任务名称",
+ "namePlaceholder": "输入任务名称",
+ "nameRequired": "请输入任务名称",
+ "nameMinLength": "任务名称至少需要 3 个字符(不含首尾空格,Label Studio 限制)",
+ "nameMaxLength": "任务名称不能超过100个字符",
+ "description": "任务描述",
+ "descriptionPlaceholder": "详细描述标注任务的要求和目标",
+ "dataset": "选择数据集",
+ "datasetRequired": "请选择数据集",
+ "template": "标注模板",
+ "templateRequired": "请选择标注模板",
+ "noTemplatesAvailable": "暂无可用模板,请先创建模板",
+ "selectTemplate": "请选择标注模板",
+ "noTemplatesFound": "暂无模板,请前往「标注模板」页面创建",
+ "selectDatasetAndFiles": "选择数据集和文件",
+ "currentDataset": "当前数据集:{name} - 已选择 {count} 个文件",
+ "currentDatasetImages": "当前数据集:{name} - 已选择 {count} 个图像文件",
+ "modelSize": "模型规模",
+ "modelSizeRequired": "请选择模型规模",
+ "confThreshold": "置信度阈值",
+ "confThresholdRequired": "请选择置信度阈值",
+ "targetClasses": "目标类别",
+ "selectAllClasses": "选中所有类别",
+ "selectTargetClasses": "选择目标类别"
+ },
+ "templateCategories": {
+ "cv": "计算机视觉",
+ "nlp": "自然语言处理"
+ },
+ "customTemplate": "自定义模板",
+ "customTemplateDesc": "创建符合特定需求的标注模板",
+ "selectedTemplate": "已选择模板",
+ "templateSelection": "模板选择",
+ "manual": "手动标注",
+ "auto": "自动标注",
+ "ok": "确定",
+ "messages": {
+ "selectAtLeastOneFile": "请至少选择一个文件",
+ "selectAtLeastOneImageFile": "请至少选择一个图像文件",
+ "createSuccess": "创建标注任务成功",
+ "createFailed": "创建失败,请稍后重试",
+ "autoCreateSuccess": "自动标注任务创建成功",
+ "autoCreateFailed": "创建自动标注任务失败",
+ "datasetTypeFiltered": "已根据模板类型筛选数据集,请重新选择数据集和文件"
+ }
+ },
+ "template": {
+ "title": "标注模板",
+ "create": "创建模板",
+ "filters": {
+ "category": "分类",
+ "dataType": "数据类型",
+ "labelingType": "标注类型",
+ "builtIn": "模板类型"
+ },
+ "columns": {
+ "name": "模板名称",
+ "description": "描述",
+ "dataType": "数据类型",
+ "labelingType": "标注类型",
+ "category": "分类",
+ "builtIn": "类型",
+ "version": "版本",
+ "createdAt": "创建时间",
+ "actions": "操作"
+ },
+ "messages": {
+ "deleteSuccess": "模板删除成功",
+ "deleteFailed": "删除模板失败",
+ "deleteConfirm": "确定要删除这个模板吗?",
+ "confirmDelete": "确定",
+ "cancelDelete": "取消"
+ },
+ "actions": {
+ "viewDetail": "查看详情",
+ "edit": "编辑",
+ "delete": "删除"
+ },
+ "searchPlaceholder": "搜索模板名称、描述"
+ },
+ "autoAnnotation": {
+ "title": "自动标注",
+ "createTask": "创建自动标注任务",
+ "columns": {
+ "name": "任务名称",
+ "dataset": "数据集",
+ "modelSize": "模型规模",
+ "confThreshold": "置信度",
+ "targetClasses": "目标类别",
+ "status": "状态",
+ "progress": "进度",
+ "detectedObjects": "检测对象数",
+ "createdAt": "创建时间",
+ "actions": "操作"
+ },
+ "statusLabels": {
+ "pending": "等待中",
+ "running": "处理中",
+ "completed": "已完成",
+ "failed": "失败",
+ "cancelled": "已取消"
+ },
+ "modelSizeLabels": {
+ "n": "YOLOv8n (最快)",
+ "s": "YOLOv8s",
+ "m": "YOLOv8m",
+ "l": "YOLOv8l (推荐)",
+ "x": "YOLOv8x (最精确)"
+ },
+ "actions": {
+ "annotate": "标注",
+ "syncToDb": "同步到数据库",
+ "exportResult": "导出标注结果",
+ "editDataset": "编辑任务数据集",
+ "delete": "删除任务",
+ "moreActions": "更多操作"
+ },
+ "confirm": {
+ "deleteTitle": "确认删除自动标注任务「{name}」吗?",
+ "deleteContent": "删除任务后,已生成的标注结果不会被删除。",
+ "deleteOkText": "删除",
+ "deleteCancelText": "取消"
+ },
+ "messages": {
+ "deleteSuccess": "自动标注任务删除成功",
+ "deleteFailed": "删除失败,请稍后重试"
+ }
+ },
+ "annotate": {
+ "title": "标注工作台",
+ "saveSuccess": "标注已保存",
+ "skipSuccess": "已跳过",
+ "backToList": "返回列表"
+ },
+ "dialogs": {
+ "editDataset": {
+ "title": "编辑数据集",
+ "description": "修改该任务的数据集文件",
+ "selectedCount": "已选择 {count} 个文件",
+ "cancel": "取消",
+ "save": "保存",
+ "loading": "保存中...",
+ "success": "数据集更新成功",
+ "failed": "数据集更新失败,请稍后重试",
+ "fetchFilesFailed": "获取任务当前数据集文件失败"
+ },
+ "importFromLS": {
+ "title": "从 Label Studio 导入",
+ "description": "从 Label Studio 导入标注结果到本地",
+ "cancel": "取消",
+ "import": "导入",
+ "loading": "导入中...",
+ "success": "导入成功",
+ "failed": "导入失败,请稍后重试"
+ },
+ "syncToDb": {
+ "loading": "正在从 Label Studio 同步标注到数据库...",
+ "success": "同步完成",
+ "fail": "同步失败,请稍后重试"
+ }
+ },
+ "const": {
+ "status": {
+ "active": "活跃",
+ "processing": "处理中",
+ "inactive": "未激活",
+ "completed": "已完成",
+ "skipped": "已跳过",
+ "pending": "待开始"
+ },
+ "dataType": {
+ "text": "文本",
+ "image": "图片",
+ "audio": "音频",
+ "video": "视频"
+ },
+ "classification": {
+ "cv": "计算机视觉",
+ "nlp": "自然语言处理",
+ "audio": "音频",
+ "qualityControl": "质量控制",
+ "custom": "自定义"
+ },
+ "annotationType": {
+ "classification": "分类",
+ "objectDetection": "目标检测",
+ "segmentation": "分割",
+ "ner": "命名实体识别"
+ },
+ "templateType": {
+ "system": "系统内置",
+ "custom": "自定义"
+ },
+ "stats": {
+ "accuracy": "准确率",
+ "averageTime": "平均时长",
+ "reviewCount": "待复核"
+ }
+ }
}
}
diff --git a/frontend/src/pages/DataAnnotation/Annotate/AnnotationWorkSpace.tsx b/frontend/src/pages/DataAnnotation/Annotate/AnnotationWorkSpace.tsx
deleted file mode 100644
index fb5910d80..000000000
--- a/frontend/src/pages/DataAnnotation/Annotate/AnnotationWorkSpace.tsx
+++ /dev/null
@@ -1,229 +0,0 @@
-import { useEffect, useState } from "react";
-import { Card, message } from "antd";
-import { Button, Badge, Progress, Checkbox } from "antd";
-import {
- ArrowLeft,
- FileText,
- ImageIcon,
- Video,
- Music,
- Save,
- SkipForward,
- CheckCircle,
- Eye,
- Settings,
-} from "lucide-react";
-import { mockTasks } from "@/mock/annotation";
-import { Outlet, useNavigate } from "react-router";
-
-export default function AnnotationWorkspace() {
- const navigate = useNavigate();
- const [task, setTask] = useState(mockTasks[0]);
-
- const [currentFileIndex, setCurrentFileIndex] = useState(0);
- const [annotationProgress, setAnnotationProgress] = useState({
- completed: task.completedCount,
- skipped: task.skippedCount,
- total: task.totalCount,
- });
-
- const handleSaveAndNext = () => {
- setAnnotationProgress((prev) => ({
- ...prev,
- completed: prev.completed + 1,
- }));
-
- if (currentFileIndex < task.totalCount - 1) {
- setCurrentFileIndex(currentFileIndex + 1);
- }
-
- message({
- title: "标注已保存",
- description: "标注结果已保存,自动跳转到下一个",
- });
- };
-
- const handleSkipAndNext = () => {
- setAnnotationProgress((prev) => ({
- ...prev,
- skipped: prev.skipped + 1,
- }));
-
- if (currentFileIndex < task.totalCount - 1) {
- setCurrentFileIndex(currentFileIndex + 1);
- }
-
- message({
- title: "已跳过",
- description: "已跳过当前项目,自动跳转到下一个",
- });
- };
-
- const getDatasetTypeIcon = (type: string) => {
- switch (type) {
- case "text":
- return ;
- case "image":
- return ;
- case "video":
- return ;
- case "audio":
- return ;
- default:
- return ;
- }
- };
-
- const currentProgress = Math.round(
- ((annotationProgress.completed + annotationProgress.skipped) /
- annotationProgress.total) *
- 100
- );
-
- return (
-
- {/* Header */}
-
-
-
-
- {getDatasetTypeIcon(task.datasetType)}
- {task.name}
-
-
-
-
- {currentFileIndex + 1} / {task.totalCount}
-
-
-
进度:
-
-
{currentProgress}%
-
-
-
-
- {/* Main Content */}
-
- {/* Annotation Area */}
-
-
-
-
- {/* Right Sidebar - Only show for text and image types */}
- {(task.datasetType === "text" || task.datasetType === "image") && (
-
- {/* Progress Stats */}
-
-
-
- 已完成
-
- {annotationProgress.completed}
-
-
-
- 已跳过
-
- {annotationProgress.skipped}
-
-
-
- 剩余
-
- {annotationProgress.total -
- annotationProgress.completed -
- annotationProgress.skipped}
-
-
-
-
- 总进度
- {currentProgress}%
-
-
-
-
- {/* Quick Actions */}
-
-
- }
- >
- 保存并下一个
-
- }
- >
- 跳过并下一个
-
- }>
- 仅保存
-
- }>
- 预览结果
-
-
-
-
- {/* Navigation */}
-
-
-
-
-
-
-
- 当前: {currentFileIndex + 1} / {task.totalCount}
-
-
-
-
- {/* Settings */}
-
-
-
- 自动保存
-
-
-
- 快捷键提示
-
-
-
}>
- 更多设置
-
-
-
-
- )}
-
-
- );
-}
diff --git a/frontend/src/pages/DataAnnotation/Annotate/components/AudioAnnotation.tsx b/frontend/src/pages/DataAnnotation/Annotate/components/AudioAnnotation.tsx
deleted file mode 100644
index 9e690c59d..000000000
--- a/frontend/src/pages/DataAnnotation/Annotate/components/AudioAnnotation.tsx
+++ /dev/null
@@ -1,713 +0,0 @@
-import { useState, useRef, useEffect } from "react";
-import { Card, Button, Badge, Slider, message } from "antd";
-import {
- Play,
- Pause,
- Square,
- SkipBack,
- SkipForward,
- Volume2,
- VolumeX,
- Scissors,
- Save,
- CheckCircle,
- Trash2,
- Edit,
- Mic,
- AudioWaveformIcon as Waveform,
-} from "lucide-react";
-
-interface AudioSegment {
- id: string;
- startTime: number;
- endTime: number;
- transcription: string;
- label: string;
- confidence?: number;
- speaker?: string;
-}
-
-interface AudioAnnotationWorkspaceProps {
- task: any;
- currentFileIndex: number;
- onSaveAndNext: () => void;
- onSkipAndNext: () => void;
-}
-
-// 模拟音频数据
-const mockAudioFiles = [
- {
- id: "1",
- name: "interview_001.wav",
- url: "/placeholder-audio.mp3", // 这里应该是实际的音频文件URL
- duration: 180, // 3分钟
- segments: [
- {
- id: "1",
- startTime: 0,
- endTime: 15,
- transcription: "你好,欢迎参加今天的访谈。请先介绍一下自己。",
- label: "问题",
- confidence: 0.95,
- speaker: "主持人",
- },
- {
- id: "2",
- startTime: 15,
- endTime: 45,
- transcription:
- "大家好,我是张三,目前在一家科技公司担任产品经理,有五年的工作经验。",
- label: "回答",
- confidence: 0.88,
- speaker: "受访者",
- },
- {
- id: "3",
- startTime: 45,
- endTime: 60,
- transcription: "很好,那么请谈谈你对人工智能发展的看法。",
- label: "问题",
- confidence: 0.92,
- speaker: "主持人",
- },
- ],
- },
-];
-
-// 预定义标签
-const audioLabels = [
- { name: "问题", color: "#3B82F6" },
- { name: "回答", color: "#10B981" },
- { name: "讨论", color: "#F59E0B" },
- { name: "总结", color: "#EF4444" },
- { name: "背景音", color: "#8B5CF6" },
- { name: "其他", color: "#6B7280" },
-];
-
-export default function AudioAnnotationWorkspace({
- task,
- currentFileIndex,
- onSaveAndNext,
- onSkipAndNext,
-}: AudioAnnotationWorkspaceProps) {
- const audioRef = useRef(null);
- const [currentAudio] = useState(mockAudioFiles[0]);
- const [segments, setSegments] = useState(
- currentAudio.segments
- );
- const [isPlaying, setIsPlaying] = useState(false);
- const [currentTime, setCurrentTime] = useState(0);
- const [duration, setDuration] = useState(currentAudio.duration);
- const [volume, setVolume] = useState(1);
- const [isMuted, setIsMuted] = useState(false);
- const [selectedSegment, setSelectedSegment] = useState(null);
- const [isCreatingSegment, setIsCreatingSegment] = useState(false);
- const [newSegmentStart, setNewSegmentStart] = useState(0);
- const [editingSegment, setEditingSegment] = useState(
- null
- );
-
- useEffect(() => {
- const audio = audioRef.current;
- if (!audio) return;
-
- const updateTime = () => setCurrentTime(audio.currentTime);
- const updateDuration = () => setDuration(audio.duration);
- const handleEnded = () => setIsPlaying(false);
-
- audio.addEventListener("timeupdate", updateTime);
- audio.addEventListener("loadedmetadata", updateDuration);
- audio.addEventListener("ended", handleEnded);
-
- return () => {
- audio.removeEventListener("timeupdate", updateTime);
- audio.removeEventListener("loadedmetadata", updateDuration);
- audio.removeEventListener("ended", handleEnded);
- };
- }, []);
-
- const togglePlayPause = () => {
- const audio = audioRef.current;
- if (!audio) return;
-
- if (isPlaying) {
- audio.pause();
- } else {
- audio.play();
- }
- setIsPlaying(!isPlaying);
- };
-
- const handleSeek = (time: number) => {
- const audio = audioRef.current;
- if (!audio) return;
-
- audio.currentTime = time;
- setCurrentTime(time);
- };
-
- const handleVolumeChange = (value: number[]) => {
- const audio = audioRef.current;
- if (!audio) return;
-
- const newVolume = value[0];
- audio.volume = newVolume;
- setVolume(newVolume);
- setIsMuted(newVolume === 0);
- };
-
- const toggleMute = () => {
- const audio = audioRef.current;
- if (!audio) return;
-
- if (isMuted) {
- audio.volume = volume;
- setIsMuted(false);
- } else {
- audio.volume = 0;
- setIsMuted(true);
- }
- };
-
- const startCreatingSegment = () => {
- setIsCreatingSegment(true);
- setNewSegmentStart(currentTime);
- toast({
- title: "开始创建片段",
- description: `片段起始时间: ${formatTime(currentTime)}`,
- });
- };
-
- const finishCreatingSegment = () => {
- if (!isCreatingSegment) return;
-
- const newSegment: AudioSegment = {
- id: Date.now().toString(),
- startTime: newSegmentStart,
- endTime: currentTime,
- transcription: "",
- label: audioLabels[0].name,
- speaker: "",
- };
-
- setSegments([...segments, newSegment]);
- setIsCreatingSegment(false);
- setEditingSegment(newSegment);
-
- toast({
- title: "片段已创建",
- description: `时长: ${formatTime(currentTime - newSegmentStart)}`,
- });
- };
-
- const deleteSegment = (id: string) => {
- setSegments(segments.filter((s) => s.id !== id));
- setSelectedSegment(null);
- toast({
- title: "片段已删除",
- description: "音频片段已被删除",
- });
- };
-
- const updateSegment = (updatedSegment: AudioSegment) => {
- setSegments(
- segments.map((s) => (s.id === updatedSegment.id ? updatedSegment : s))
- );
- setEditingSegment(null);
- toast({
- title: "片段已更新",
- description: "转录内容已保存",
- });
- };
-
- const playSegment = (segment: AudioSegment) => {
- handleSeek(segment.startTime);
- setSelectedSegment(segment.id);
-
- const audio = audioRef.current;
- if (!audio) return;
-
- audio.play();
- setIsPlaying(true);
-
- // 在片段结束时暂停
- const checkEnd = () => {
- if (audio.currentTime >= segment.endTime) {
- audio.pause();
- setIsPlaying(false);
- audio.removeEventListener("timeupdate", checkEnd);
- }
- };
- audio.addEventListener("timeupdate", checkEnd);
- };
-
- const formatTime = (seconds: number) => {
- const mins = Math.floor(seconds / 60);
- const secs = Math.floor(seconds % 60);
- return `${mins.toString().padStart(2, "0")}:${secs
- .toString()
- .padStart(2, "0")}`;
- };
-
- const getSegmentColor = (label: string) => {
- const labelConfig = audioLabels.find((l) => l.name === label);
- return labelConfig?.color || "#6B7280";
- };
-
- return (
-
- {/* Audio Player */}
-
-
- {/* Audio Element */}
-
-
- {/* Player Controls */}
-
-
-
-
-
-
- {/* Timeline */}
-
-
- {formatTime(currentTime)}
- {formatTime(duration)}
-
-
-
handleSeek(value[0])}
- className="w-full"
- />
- {/* Segment Visualization */}
-
- {segments.map((segment) => {
- const left = (segment.startTime / duration) * 100;
- const width =
- ((segment.endTime - segment.startTime) / duration) * 100;
- return (
-
- );
- })}
-
- {/* Current Creating Segment */}
- {isCreatingSegment && (
-
- )}
-
-
-
- {/* Volume Control */}
-
-
-
-
-
- {/* Annotation Controls */}
-
- {isCreatingSegment ? (
-
- ) : (
-
- )}
-
-
-
-
- {/* Main Content */}
-
- {/* Segments List */}
-
-
-
-
音频片段
- {segments.length} 个片段
-
-
-
-
- {segments.map((segment) => (
-
setSelectedSegment(segment.id)}
- >
-
-
-
-
-
-
- {segment.label}
-
-
-
-
-
-
-
-
-
- {formatTime(segment.startTime)} -{" "}
- {formatTime(segment.endTime)}
- {segment.speaker && ` | ${segment.speaker}`}
-
-
- {segment.transcription || "未转录"}
-
- {segment.confidence && (
-
-
- 置信度:
-
-
- {(segment.confidence * 100).toFixed(1)}%
-
-
- )}
-
-
-
- ))}
-
-
-
-
-
- {/* Transcription Editor */}
-
- {editingSegment ? (
-
-
-
-
- 编辑转录
-
-
-
-
-
-
-
-
-
-
-
-
-
- setEditingSegment({
- ...editingSegment,
- speaker: e.target.value,
- })
- }
- placeholder="输入说话人名称"
- className="mt-1"
- />
-
-
-
-
-
-
-
-
-
-
-
-
- ) : selectedSegment ? (
-
-
-
-
- 片段详情
-
-
-
- {(() => {
- const segment = segments.find(
- (s) => s.id === selectedSegment
- );
- if (!segment) return null;
-
- return (
-
-
-
-
- 时间范围
-
-
- {formatTime(segment.startTime)} -{" "}
- {formatTime(segment.endTime)}
-
-
-
-
时长
-
- {formatTime(segment.endTime - segment.startTime)}
-
-
-
-
-
-
- {segment.speaker && (
-
-
说话人
-
{segment.speaker}
-
- )}
-
-
-
转录内容
-
- {segment.transcription || "暂无转录内容"}
-
-
-
- {segment.confidence && (
-
-
置信度
-
- {(segment.confidence * 100).toFixed(1)}%
-
-
- )}
-
-
-
-
-
-
- );
- })()}
-
-
- ) : (
-
-
-
-
- 音频标注工作区
-
-
- 选择一个音频片段开始编辑转录内容
-
-
-
• 点击"创建片段"开始标记音频片段
-
• 选择片段进行转录和标注
-
• 使用播放控件精确定位音频位置
-
-
-
- )}
-
-
-
- {/* Bottom Actions */}
-
-
-
- 文件: {currentAudio.name} | 片段: {segments.length} | 总时长:{" "}
- {formatTime(duration)}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/DataAnnotation/Annotate/components/ImageAnnotation.tsx b/frontend/src/pages/DataAnnotation/Annotate/components/ImageAnnotation.tsx
deleted file mode 100644
index 8d95e2bdf..000000000
--- a/frontend/src/pages/DataAnnotation/Annotate/components/ImageAnnotation.tsx
+++ /dev/null
@@ -1,617 +0,0 @@
-import type React from "react";
-import { useState, useRef, useEffect } from "react";
-import { Button, Badge, Checkbox, message } from "antd";
-import {
- Square,
- Circle,
- MousePointer,
- ZoomIn,
- ZoomOut,
- RotateCcw,
- ArrowLeft,
- ArrowRight,
- MoreHorizontal,
-} from "lucide-react";
-
-interface Annotation {
- id: string;
- type: "rectangle" | "circle" | "polygon";
- label: string;
- color: string;
- coordinates: number[];
- visible: boolean;
-}
-
-interface ImageAnnotationWorkspaceProps {
- task: any;
- currentFileIndex: number;
- onSaveAndNext: () => void;
- onSkipAndNext: () => void;
-}
-
-// 模拟医学图像数据
-const mockMedicalImages = [
- {
- id: "1",
- name: "2024-123456",
- thumbnail: "/placeholder.svg?height=60&width=60&text=Slide1",
- url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/img_v3_02oi_e6dd5540-9ca4-4277-ad2b-4debaa1c8ddg.jpg-oibLbUmFpZMkLTmwZB7lT1UWKFlOLA.jpeg",
- },
- {
- id: "2",
- name: "2024-234567",
- thumbnail: "/placeholder.svg?height=60&width=60&text=Slide2",
- url: "/placeholder.svg?height=600&width=800&text=Medical Image 2",
- },
- {
- id: "3",
- name: "2025-345678",
- thumbnail: "/placeholder.svg?height=60&width=60&text=Slide3",
- url: "/placeholder.svg?height=600&width=800&text=Medical Image 3",
- },
- {
- id: "4",
- name: "1234-123456",
- thumbnail: "/placeholder.svg?height=60&width=60&text=Slide4",
- url: "/placeholder.svg?height=600&width=800&text=Medical Image 4",
- },
- {
- id: "5",
- name: "2025-456789",
- thumbnail: "/placeholder.svg?height=60&width=60&text=Slide5",
- url: "/placeholder.svg?height=600&width=800&text=Medical Image 5",
- },
- {
- id: "6",
- name: "2025-567890",
- thumbnail: "/placeholder.svg?height=60&width=60&text=Slide6",
- url: "/placeholder.svg?height=600&width=800&text=Medical Image 6",
- },
- {
- id: "7",
- name: "2025-678901",
- thumbnail: "/placeholder.svg?height=60&width=60&text=Slide7",
- url: "/placeholder.svg?height=600&width=800&text=Medical Image 7",
- },
- {
- id: "8",
- name: "2025-789012",
- thumbnail: "/placeholder.svg?height=60&width=60&text=Slide8",
- url: "/placeholder.svg?height=600&width=800&text=Medical Image 8",
- },
- {
- id: "9",
- name: "2025-890123",
- thumbnail: "/placeholder.svg?height=60&width=60&text=Slide9",
- url: "/placeholder.svg?height=600&width=800&text=Medical Image 9",
- },
- {
- id: "10",
- name: "2025-901234",
- thumbnail: "/placeholder.svg?height=60&width=60&text=Slide10",
- url: "/placeholder.svg?height=600&width=800&text=Medical Image 10",
- },
-];
-
-// 医学标注选项
-const medicalAnnotationOptions = [
- {
- id: "tumor_present",
- label: "是否有肿瘤",
- type: "radio",
- options: ["是", "否"],
- },
- {
- id: "tumor_type",
- label: "肿瘤形成",
- type: "checkbox",
- options: ["腺管形成"],
- },
- { id: "grade_1", label: "1级", type: "checkbox", options: ["1[x]"] },
- { id: "grade_2", label: "2级", type: "checkbox", options: ["2[x]"] },
- { id: "remarks", label: "备注", type: "textarea" },
- {
- id: "nuclear_polymorphism",
- label: "核多形性",
- type: "checkbox",
- options: ["核分裂象"],
- },
- {
- id: "histological_type",
- label: "组织学类型",
- type: "checkbox",
- options: ["1[b]", "2[y]", "3[t]"],
- },
- {
- id: "small_time_lesion",
- label: "小时病位置[3]",
- type: "checkbox",
- options: ["1[b]", "2[y]", "3[t]"],
- },
- {
- id: "ductal_position",
- label: "导管原位置[4]",
- type: "checkbox",
- options: ["1[o]", "2[p]", "3[t]"],
- },
- {
- id: "ductal_position_large",
- label: "导管原位置件大于腺分",
- type: "checkbox",
- options: ["腺分裂象"],
- },
- {
- id: "mitosis",
- label: "化[5]",
- type: "checkbox",
- options: ["1[o]", "2[p]", "3[t]"],
- },
- {
- id: "original_position",
- label: "原位实性乳头状[6]",
- type: "checkbox",
- options: ["1[o]", "2[p]", "3[t]"],
- },
- {
- id: "infiltrating_lesion",
- label: "浸润性病(非特殊型)[7]",
- type: "checkbox",
- options: ["1[o]", "2[p]", "3[t]"],
- },
- {
- id: "infiltrating_small",
- label: "浸润性小叶癌[8]",
- type: "checkbox",
- options: ["脉管侵犯"],
- },
- {
- id: "infiltrating_real",
- label: "浸润实性乳头状癌[9]",
- type: "checkbox",
- options: ["1[o]", "2[p]", "3[t]"],
- },
- {
- id: "other_lesion",
- label: "其他病[0]",
- type: "checkbox",
- options: ["+[k]"],
- },
-];
-
-export default function ImageAnnotationWorkspace({
- currentFileIndex,
-}: ImageAnnotationWorkspaceProps) {
- const canvasRef = useRef(null);
- const [selectedImageIndex, setSelectedImageIndex] = useState(
- currentFileIndex || 0
- );
- const [currentImage, setCurrentImage] = useState(
- mockMedicalImages[selectedImageIndex]
- );
- const [annotations, setAnnotations] = useState([]);
- const [selectedTool, setSelectedTool] = useState<
- "select" | "rectangle" | "circle"
- >("select");
- const [isDrawing, setIsDrawing] = useState(false);
- const [startPoint, setStartPoint] = useState({ x: 0, y: 0 });
- const [zoom, setZoom] = useState(1);
- const [pan, setPan] = useState({ x: 0, y: 0 });
- const [selectedAnnotation, setSelectedAnnotation] = useState(
- null
- );
- const [annotationValues, setAnnotationValues] = useState>(
- {}
- );
-
- useEffect(() => {
- setCurrentImage(mockMedicalImages[selectedImageIndex]);
- drawCanvas();
- }, [selectedImageIndex, annotations, zoom, pan]);
-
- const drawCanvas = () => {
- const canvas = canvasRef.current;
- if (!canvas) return;
-
- const ctx = canvas.getContext("2d");
- if (!ctx) return;
-
- ctx.clearRect(0, 0, canvas.width, canvas.height);
-
- const img = new Image();
- img.crossOrigin = "anonymous";
- img.onload = () => {
- ctx.save();
- ctx.scale(zoom, zoom);
- ctx.translate(pan.x, pan.y);
- ctx.drawImage(img, 0, 0, canvas.width / zoom, canvas.height / zoom);
-
- // 绘制标注
- annotations.forEach((annotation) => {
- if (!annotation.visible) return;
-
- ctx.strokeStyle = annotation.color;
- ctx.fillStyle = annotation.color + "20";
- ctx.lineWidth = 2;
-
- if (annotation.type === "rectangle") {
- const [x, y, width, height] = annotation.coordinates;
- ctx.strokeRect(x, y, width, height);
- ctx.fillRect(x, y, width, height);
- } else if (annotation.type === "circle") {
- const [centerX, centerY, radius] = annotation.coordinates;
- ctx.beginPath();
- ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
- ctx.stroke();
- ctx.fill();
- }
-
- if (selectedAnnotation === annotation.id) {
- ctx.strokeStyle = "#FF0000";
- ctx.lineWidth = 3;
- ctx.setLineDash([5, 5]);
-
- if (annotation.type === "rectangle") {
- const [x, y, width, height] = annotation.coordinates;
- ctx.strokeRect(x - 2, y - 2, width + 4, height + 4);
- } else if (annotation.type === "circle") {
- const [centerX, centerY, radius] = annotation.coordinates;
- ctx.beginPath();
- ctx.arc(centerX, centerY, radius + 2, 0, 2 * Math.PI);
- ctx.stroke();
- }
-
- ctx.setLineDash([]);
- }
- });
-
- ctx.restore();
- };
- img.src = currentImage.url;
- };
-
- const handleCanvasMouseDown = (e: React.MouseEvent) => {
- const canvas = canvasRef.current;
- if (!canvas) return;
-
- const rect = canvas.getBoundingClientRect();
- const x = (e.clientX - rect.left - pan.x) / zoom;
- const y = (e.clientY - rect.top - pan.y) / zoom;
-
- if (selectedTool === "rectangle" || selectedTool === "circle") {
- setIsDrawing(true);
- setStartPoint({ x, y });
- } else if (selectedTool === "select") {
- const clickedAnnotation = annotations.find((annotation) => {
- if (annotation.type === "rectangle") {
- const [ax, ay, width, height] = annotation.coordinates;
- return x >= ax && x <= ax + width && y >= ay && y <= ay + height;
- } else if (annotation.type === "circle") {
- const [centerX, centerY, radius] = annotation.coordinates;
- const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
- return distance <= radius;
- }
- return false;
- });
-
- setSelectedAnnotation(clickedAnnotation?.id || null);
- }
- };
-
- const handleCanvasMouseMove = (e: React.MouseEvent) => {
- if (!isDrawing) return;
-
- const canvas = canvasRef.current;
- if (!canvas) return;
-
- const rect = canvas.getBoundingClientRect();
- const x = (e.clientX - rect.left - pan.x) / zoom;
- const y = (e.clientY - rect.top - pan.y) / zoom;
-
- drawCanvas();
- const ctx = canvas.getContext("2d");
- if (!ctx) return;
-
- ctx.save();
- ctx.scale(zoom, zoom);
- ctx.translate(pan.x, pan.y);
- ctx.strokeStyle = "#3B82F6";
- ctx.lineWidth = 2;
- ctx.setLineDash([5, 5]);
-
- if (selectedTool === "rectangle") {
- const width = x - startPoint.x;
- const height = y - startPoint.y;
- ctx.strokeRect(startPoint.x, startPoint.y, width, height);
- } else if (selectedTool === "circle") {
- const radius = Math.sqrt(
- (x - startPoint.x) ** 2 + (y - startPoint.y) ** 2
- );
- ctx.beginPath();
- ctx.arc(startPoint.x, startPoint.y, radius, 0, 2 * Math.PI);
- ctx.stroke();
- }
-
- ctx.restore();
- };
-
- const handleCanvasMouseUp = (e: React.MouseEvent) => {
- if (!isDrawing) return;
-
- const canvas = canvasRef.current;
- if (!canvas) return;
-
- const rect = canvas.getBoundingClientRect();
- const x = (e.clientX - rect.left - pan.x) / zoom;
- const y = (e.clientY - rect.top - pan.y) / zoom;
-
- let coordinates: number[] = [];
-
- if (selectedTool === "rectangle") {
- const width = x - startPoint.x;
- const height = y - startPoint.y;
- coordinates = [startPoint.x, startPoint.y, width, height];
- } else if (selectedTool === "circle") {
- const radius = Math.sqrt(
- (x - startPoint.x) ** 2 + (y - startPoint.y) ** 2
- );
- coordinates = [startPoint.x, startPoint.y, radius];
- }
-
- if (coordinates.length > 0) {
- const newAnnotation: Annotation = {
- id: Date.now().toString(),
- type: selectedTool as "rectangle" | "circle",
- label: "标注",
- color: "#3B82F6",
- coordinates,
- visible: true,
- };
-
- setAnnotations([...annotations, newAnnotation]);
- }
-
- setIsDrawing(false);
- };
-
- const handleAnnotationValueChange = (optionId: string, value: any) => {
- setAnnotationValues((prev) => ({
- ...prev,
- [optionId]: value,
- }));
- };
-
- const handleUpdate = () => {
- message({
- title: "标注已更新",
- description: "医学标注信息已保存",
- });
- };
-
- return (
-
- {/* Left Sidebar - Image List */}
-
- {/* Header */}
-
-
-
- image
- img
-
-
- case_id
- #13754
-
-
-
-
-
-
- {/* Image List */}
-
- {mockMedicalImages.map((image, index) => (
-
setSelectedImageIndex(index)}
- >
-
- {index + 1}
-
-

-
-
- ))}
-
-
-
- {/* Main Content Area */}
-
- {/* Main Image Display */}
-
-
-
-
WSI图像预览
-
-
- 病理号: 1234-123456
-
-
- 取材部位: 余乳
-
-
-
-
-
-
-
- {/* Zoom Controls */}
-
-
- {Math.round(zoom * 100)}%
-
-
-
-
- {/* Tool Selection */}
-
-
-
-
-
-
-
- {/* Navigation Controls */}
-
-
-
-
-
- {/* Right Sidebar - Annotation Panel */}
-
-
-
-
标注
-
-
- {medicalAnnotationOptions.map((option) => (
-
-
{option.label}
-
- {option.type === "radio" && (
-
- {option.options?.map((opt) => (
-
-
- handleAnnotationValueChange(
- option.id,
- e.target.value
- )
- }
- className="w-4 h-4"
- />
- {opt}
-
- ))}
-
- )}
-
- {option.type === "checkbox" && (
-
- {option.options?.map((opt) => (
-
-
- handleAnnotationValueChange(
- `${option.id}_${opt}`,
- checked
- )
- }
- />
- {opt}
-
- ))}
-
- )}
-
- {option.type === "textarea" && (
-
- ))}
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/DataAnnotation/Annotate/components/TextAnnotation.tsx b/frontend/src/pages/DataAnnotation/Annotate/components/TextAnnotation.tsx
deleted file mode 100644
index e0ab71dc7..000000000
--- a/frontend/src/pages/DataAnnotation/Annotate/components/TextAnnotation.tsx
+++ /dev/null
@@ -1,457 +0,0 @@
-import { useState } from "react";
-import { Card, Button, Badge, Input, Checkbox } from "antd";
-
-import {
- File,
- Search,
- CheckCircle,
- ThumbsUp,
- ThumbsDown,
- MessageSquare,
- HelpCircle,
-} from "lucide-react";
-
-interface QAPair {
- id: string;
- question: string;
- answer: string;
- status: "pending" | "approved" | "rejected";
- confidence?: number;
-}
-
-interface FileData {
- id: string;
- name: string;
- qaPairs: QAPair[];
-}
-
-interface TextAnnotationWorkspaceProps {
- task: any;
- currentFileIndex: number;
- onSaveAndNext: () => void;
- onSkipAndNext: () => void;
-}
-
-// 模拟文件数据
-const mockFiles: FileData[] = [
- {
- id: "1",
- name: "document_001.txt",
- qaPairs: [
- {
- id: "1",
- question: "什么是人工智能?",
- answer:
- "人工智能(AI)是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务的系统。",
- status: "pending",
- confidence: 0.85,
- },
- {
- id: "2",
- question: "机器学习和深度学习有什么区别?",
- answer:
- "机器学习是人工智能的一个子集,而深度学习是机器学习的一个子集。深度学习使用神经网络来模拟人脑的工作方式。",
- status: "pending",
- confidence: 0.92,
- },
- {
- id: "3",
- question: "什么是神经网络?",
- answer:
- "神经网络是一种受生物神经网络启发的计算模型,由相互连接的节点(神经元)组成,能够学习和识别模式。",
- status: "pending",
- confidence: 0.78,
- },
- ],
- },
- {
- id: "2",
- name: "document_002.txt",
- qaPairs: [
- {
- id: "4",
- question: "什么是自然语言处理?",
- answer:
- "自然语言处理(NLP)是人工智能的一个分支,专注于使计算机能够理解、解释和生成人类语言。",
- status: "pending",
- confidence: 0.88,
- },
- {
- id: "5",
- question: "计算机视觉的应用有哪些?",
- answer:
- "计算机视觉广泛应用于图像识别、人脸识别、自动驾驶、医学影像分析、安防监控等领域。",
- status: "pending",
- confidence: 0.91,
- },
- ],
- },
-];
-
-export default function TextAnnotationWorkspace({
- onSaveAndNext,
- onSkipAndNext,
-}: TextAnnotationWorkspaceProps) {
- const [selectedFile, setSelectedFile] = useState(
- mockFiles[0]
- );
- const [searchQuery, setSearchQuery] = useState("");
- const [statusFilter, setStatusFilter] = useState("all");
- const [selectedQAs, setSelectedQAs] = useState([]);
-
- const handleFileSelect = (file: FileData) => {
- setSelectedFile(file);
- setSelectedQAs([]);
- };
-
- const handleQAStatusChange = (
- qaId: string,
- status: "approved" | "rejected"
- ) => {
- if (selectedFile) {
- const updatedFile = {
- ...selectedFile,
- qaPairs: selectedFile.qaPairs.map((qa) =>
- qa.id === qaId ? { ...qa, status } : qa
- ),
- };
- setSelectedFile(updatedFile);
-
- message({
- title: status === "approved" ? "已标记为留用" : "已标记为不留用",
- description: `QA对 "${qaId}" 状态已更新`,
- });
- }
- };
-
- const handleBatchApprove = () => {
- if (selectedFile && selectedQAs.length > 0) {
- const updatedFile = {
- ...selectedFile,
- qaPairs: selectedFile.qaPairs.map((qa) =>
- selectedQAs.includes(qa.id)
- ? { ...qa, status: "approved" as const }
- : qa
- ),
- };
- setSelectedFile(updatedFile);
- setSelectedQAs([]);
-
- message({
- title: "批量操作完成",
- description: `已将 ${selectedQAs.length} 个QA对标记为留用`,
- });
- }
- };
-
- const handleBatchReject = () => {
- if (selectedFile && selectedQAs.length > 0) {
- const updatedFile = {
- ...selectedFile,
- qaPairs: selectedFile.qaPairs.map((qa) =>
- selectedQAs.includes(qa.id)
- ? { ...qa, status: "rejected" as const }
- : qa
- ),
- };
- setSelectedFile(updatedFile);
- setSelectedQAs([]);
-
- message({
- title: "批量操作完成",
- description: `已将 ${selectedQAs.length} 个QA对标记为不留用`,
- });
- }
- };
-
- const handleQASelect = (qaId: string, checked: boolean) => {
- if (checked) {
- setSelectedQAs([...selectedQAs, qaId]);
- } else {
- setSelectedQAs(selectedQAs.filter((id) => id !== qaId));
- }
- };
-
- const handleSelectAll = (checked: boolean) => {
- if (checked && selectedFile) {
- setSelectedQAs(selectedFile.qaPairs.map((qa) => qa.id));
- } else {
- setSelectedQAs([]);
- }
- };
-
- const getStatusBadge = (status: string) => {
- switch (status) {
- case "approved":
- return 留用;
- case "rejected":
- return 不留用;
- default:
- return 待标注;
- }
- };
-
- const getConfidenceColor = (confidence?: number) => {
- if (!confidence) return "text-gray-500";
- if (confidence >= 0.8) return "text-green-600";
- if (confidence >= 0.6) return "text-yellow-600";
- return "text-red-600";
- };
-
- const filteredQAs =
- selectedFile?.qaPairs.filter((qa) => {
- const matchesSearch =
- qa.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
- qa.answer.toLowerCase().includes(searchQuery.toLowerCase());
- const matchesStatus =
- statusFilter === "all" || qa.status === statusFilter;
- return matchesSearch && matchesStatus;
- }) || [];
-
- return (
-
- {/* File List */}
-
-
-
-
-
-
- {mockFiles.map((file) => (
-
handleFileSelect(file)}
- >
-
-
-
-
{file.name}
-
- {file.qaPairs.length} 个QA对
-
-
-
-
- ))}
-
-
-
-
-
- {/* QA Annotation Area */}
-
- {selectedFile ? (
-
- {/* Header */}
-
-
-
{selectedFile.name}
-
- 共 {selectedFile.qaPairs.length} 个QA对
-
-
-
-
-
-
-
-
- {/* Filters and Batch Actions */}
-
-
-
-
-
- setSearchQuery(e.target.value)}
- className="pl-10 w-64"
- />
-
-
-
-
- {selectedQAs.length > 0 && (
-
-
- 已选择 {selectedQAs.length} 个
-
-
-
-
- )}
-
-
-
- {/* QA List */}
-
-
- 0
- }
- onChange={handleSelectAll}
- />
- 全选
-
-
-
-
- {filteredQAs.map((qa) => (
-
-
-
-
-
- handleQASelect(qa.id, checked as boolean)
- }
- />
-
-
- QA-{qa.id}
-
-
-
- {qa.confidence && (
-
- 置信度: {(qa.confidence * 100).toFixed(1)}%
-
- )}
- {getStatusBadge(qa.status)}
-
-
-
-
-
-
-
-
- 问题
-
-
-
- {qa.question}
-
-
-
-
-
-
-
- 答案
-
-
-
- {qa.answer}
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
- ) : (
-
-
-
-
- 选择文件开始标注
-
-
- 从左侧文件列表中选择一个文件开始标注工作
-
-
-
- )}
-
-
- );
-}
diff --git a/frontend/src/pages/DataAnnotation/Annotate/components/VideoAnnotation.tsx b/frontend/src/pages/DataAnnotation/Annotate/components/VideoAnnotation.tsx
deleted file mode 100644
index e0f112c3f..000000000
--- a/frontend/src/pages/DataAnnotation/Annotate/components/VideoAnnotation.tsx
+++ /dev/null
@@ -1,688 +0,0 @@
-
-
-import type React from "react";
-
-import { useState, useRef, useEffect } from "react";
-import { Card, Button, Badge, Slider, message } from "antd";
-import {
- Play,
- Pause,
- Square,
- SkipBack,
- SkipForward,
- Volume2,
- VolumeX,
- MousePointer,
- CheckCircle,
- Trash2,
- Eye,
- EyeOff,
- Target,
- Maximize,
-} from "lucide-react";
-
-interface VideoAnnotation {
- id: string;
- frameTime: number;
- type: "rectangle" | "point" | "polygon";
- coordinates: number[];
- label: string;
- color: string;
- trackId?: string;
- visible: boolean;
-}
-
-interface VideoTrack {
- id: string;
- label: string;
- color: string;
- annotations: VideoAnnotation[];
- startTime: number;
- endTime: number;
-}
-
-interface VideoAnnotationWorkspaceProps {
- task: any;
- currentFileIndex: number;
- onSaveAndNext: () => void;
- onSkipAndNext: () => void;
-}
-
-// 模拟视频数据
-const mockVideoFiles = [
- {
- id: "1",
- name: "traffic_scene_001.mp4",
- url: "/placeholder-video.mp4", // 这里应该是实际的视频文件URL
- duration: 120, // 2分钟
- fps: 30,
- width: 1920,
- height: 1080,
- },
-];
-
-// 预定义标签
-const videoLabels = [
- { name: "车辆", color: "#3B82F6" },
- { name: "行人", color: "#10B981" },
- { name: "自行车", color: "#F59E0B" },
- { name: "交通灯", color: "#EF4444" },
- { name: "路标", color: "#8B5CF6" },
- { name: "其他", color: "#6B7280" },
-];
-
-export default function VideoAnnotationWorkspace({
- task,
- currentFileIndex,
- onSaveAndNext,
- onSkipAndNext,
-}: VideoAnnotationWorkspaceProps) {
- const [messageApi, contextHolder] = message.useMessage();
- const videoRef = useRef(null);
- const canvasRef = useRef(null);
- const [currentVideo] = useState(mockVideoFiles[0]);
- const [tracks, setTracks] = useState([]);
- const [isPlaying, setIsPlaying] = useState(false);
- const [currentTime, setCurrentTime] = useState(0);
- const [duration, setDuration] = useState(currentVideo.duration);
- const [volume, setVolume] = useState(1);
- const [isMuted, setIsMuted] = useState(false);
- const [selectedTool, setSelectedTool] = useState<
- "select" | "rectangle" | "point"
- >("select");
- const [selectedLabel, setSelectedLabel] = useState(videoLabels[0]);
- const [selectedTrack, setSelectedTrack] = useState(null);
- const [isDrawing, setIsDrawing] = useState(false);
- const [startPoint, setStartPoint] = useState({ x: 0, y: 0 });
- const [playbackSpeed, setPlaybackSpeed] = useState(1);
- const [isFullscreen, setIsFullscreen] = useState(false);
-
- useEffect(() => {
- const video = videoRef.current;
- if (!video) return;
-
- const updateTime = () => setCurrentTime(video.currentTime);
- const updateDuration = () => setDuration(video.duration);
- const handleEnded = () => setIsPlaying(false);
-
- video.addEventListener("timeupdate", updateTime);
- video.addEventListener("loadedmetadata", updateDuration);
- video.addEventListener("ended", handleEnded);
-
- return () => {
- video.removeEventListener("timeupdate", updateTime);
- video.removeEventListener("loadedmetadata", updateDuration);
- video.removeEventListener("ended", handleEnded);
- };
- }, []);
-
- useEffect(() => {
- drawCanvas();
- }, [currentTime, tracks, selectedTrack]);
-
- const drawCanvas = () => {
- const canvas = canvasRef.current;
- const video = videoRef.current;
- if (!canvas || !video) return;
-
- const ctx = canvas.getContext("2d");
- if (!ctx) return;
-
- // 清空画布
- ctx.clearRect(0, 0, canvas.width, canvas.height);
-
- // 绘制当前帧的标注
- tracks.forEach((track) => {
- if (!track.annotations.length) return;
-
- // 找到当前时间最近的标注
- const currentAnnotation = track.annotations
- .filter((ann) => Math.abs(ann.frameTime - currentTime) < 0.1)
- .sort(
- (a, b) =>
- Math.abs(a.frameTime - currentTime) -
- Math.abs(b.frameTime - currentTime)
- )[0];
-
- if (!currentAnnotation || !currentAnnotation.visible) return;
-
- ctx.strokeStyle = track.color;
- ctx.fillStyle = track.color + "20";
- ctx.lineWidth = selectedTrack === track.id ? 3 : 2;
-
- if (currentAnnotation.type === "rectangle") {
- const [x, y, width, height] = currentAnnotation.coordinates;
- ctx.strokeRect(x, y, width, height);
- ctx.fillRect(x, y, width, height);
-
- // 绘制标签
- ctx.fillStyle = track.color;
- ctx.fillRect(x, y - 20, ctx.measureText(track.label).width + 8, 20);
- ctx.fillStyle = "white";
- ctx.font = "12px Arial";
- ctx.fillText(track.label, x + 4, y - 6);
- } else if (currentAnnotation.type === "point") {
- const [x, y] = currentAnnotation.coordinates;
- ctx.beginPath();
- ctx.arc(x, y, 5, 0, 2 * Math.PI);
- ctx.fill();
- ctx.stroke();
-
- // 绘制标签
- ctx.fillStyle = track.color;
- ctx.fillRect(
- x + 10,
- y - 10,
- ctx.measureText(track.label).width + 8,
- 20
- );
- ctx.fillStyle = "white";
- ctx.font = "12px Arial";
- ctx.fillText(track.label, x + 14, y + 4);
- }
- });
- };
-
- const togglePlayPause = () => {
- const video = videoRef.current;
- if (!video) return;
-
- if (isPlaying) {
- video.pause();
- } else {
- video.play();
- }
- setIsPlaying(!isPlaying);
- };
-
- const handleSeek = (time: number) => {
- const video = videoRef.current;
- if (!video) return;
-
- video.currentTime = time;
- setCurrentTime(time);
- };
-
- const handleVolumeChange = (value: number[]) => {
- const video = videoRef.current;
- if (!video) return;
-
- const newVolume = value[0];
- video.volume = newVolume;
- setVolume(newVolume);
- setIsMuted(newVolume === 0);
- };
-
- const toggleMute = () => {
- const video = videoRef.current;
- if (!video) return;
-
- if (isMuted) {
- video.volume = volume;
- setIsMuted(false);
- } else {
- video.volume = 0;
- setIsMuted(true);
- }
- };
-
- const handleSpeedChange = (speed: number) => {
- const video = videoRef.current;
- if (!video) return;
-
- video.playbackRate = speed;
- setPlaybackSpeed(speed);
- };
-
- const handleCanvasMouseDown = (e: React.MouseEvent) => {
- if (selectedTool === "select") return;
-
- const canvas = canvasRef.current;
- if (!canvas) return;
-
- const rect = canvas.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
-
- if (selectedTool === "point") {
- createPointAnnotation(x, y);
- } else if (selectedTool === "rectangle") {
- setIsDrawing(true);
- setStartPoint({ x, y });
- }
- };
-
- const handleCanvasMouseMove = (e: React.MouseEvent) => {
- if (!isDrawing || selectedTool !== "rectangle") return;
-
- const canvas = canvasRef.current;
- if (!canvas) return;
-
- const rect = canvas.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
-
- // 实时预览
- drawCanvas();
- const ctx = canvas.getContext("2d");
- if (!ctx) return;
-
- ctx.strokeStyle = selectedLabel.color;
- ctx.lineWidth = 2;
- ctx.setLineDash([5, 5]);
- ctx.strokeRect(
- startPoint.x,
- startPoint.y,
- x - startPoint.x,
- y - startPoint.y
- );
- ctx.setLineDash([]);
- };
-
- const handleCanvasMouseUp = (e: React.MouseEvent) => {
- if (!isDrawing || selectedTool !== "rectangle") return;
-
- const canvas = canvasRef.current;
- if (!canvas) return;
-
- const rect = canvas.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
-
- const width = x - startPoint.x;
- const height = y - startPoint.y;
-
- if (Math.abs(width) > 10 && Math.abs(height) > 10) {
- createRectangleAnnotation(startPoint.x, startPoint.y, width, height);
- }
-
- setIsDrawing(false);
- };
-
- const createPointAnnotation = (x: number, y: number) => {
- const newAnnotation: VideoAnnotation = {
- id: Date.now().toString(),
- frameTime: currentTime,
- type: "point",
- coordinates: [x, y],
- label: selectedLabel.name,
- color: selectedLabel.color,
- visible: true,
- };
-
- const newTrack: VideoTrack = {
- id: Date.now().toString(),
- label: selectedLabel.name,
- color: selectedLabel.color,
- annotations: [newAnnotation],
- startTime: currentTime,
- endTime: currentTime,
- };
-
- setTracks([...tracks, newTrack]);
- messageApi({
- title: "点标注已添加",
- description: `在时间 ${formatTime(currentTime)} 添加了点标注`,
- });
- };
-
- const createRectangleAnnotation = (
- x: number,
- y: number,
- width: number,
- height: number
- ) => {
- const newAnnotation: VideoAnnotation = {
- id: Date.now().toString(),
- frameTime: currentTime,
- type: "rectangle",
- coordinates: [x, y, width, height],
- label: selectedLabel.name,
- color: selectedLabel.color,
- visible: true,
- };
-
- const newTrack: VideoTrack = {
- id: Date.now().toString(),
- label: selectedLabel.name,
- color: selectedLabel.color,
- annotations: [newAnnotation],
- startTime: currentTime,
- endTime: currentTime,
- };
-
- setTracks([...tracks, newTrack]);
- messageApi.success(`在时间 ${formatTime(currentTime)} 添加了矩形标注`);
- };
-
- const deleteTrack = (trackId: string) => {
- setTracks(tracks.filter((t) => t.id !== trackId));
- setSelectedTrack(null);
- messageApi.success("标注轨迹已被删除");
- };
-
- const toggleTrackVisibility = (trackId: string) => {
- setTracks(
- tracks.map((track) =>
- track.id === trackId
- ? {
- ...track,
- annotations: track.annotations.map((ann) => ({
- ...ann,
- visible: !ann.visible,
- })),
- }
- : track
- )
- );
- };
-
- const formatTime = (seconds: number) => {
- const mins = Math.floor(seconds / 60);
- const secs = Math.floor(seconds % 60);
- return `${mins.toString().padStart(2, "0")}:${secs
- .toString()
- .padStart(2, "0")}`;
- };
-
- const toggleFullscreen = () => {
- const video = videoRef.current;
- if (!video) return;
-
- if (!isFullscreen) {
- if (video.requestFullscreen) {
- video.requestFullscreen();
- }
- } else {
- if (document.exitFullscreen) {
- document.exitFullscreen();
- }
- }
- setIsFullscreen(!isFullscreen);
- };
-
- return (
-
- {/* Tools Panel */}
-
- {/* Tool Selection */}
-
-
- 工具
-
-
-
-
-
-
-
-
- {/* Labels */}
-
-
- 标签
-
-
- {videoLabels.map((label) => (
-
- ))}
-
-
-
- {/* Playback Speed */}
-
-
- 播放速度
-
-
- {[0.25, 0.5, 1, 1.5, 2].map((speed) => (
-
- ))}
-
-
-
- {/* Tracks List */}
-
-
- 标注轨迹
-
-
-
-
- {tracks.map((track) => (
-
setSelectedTrack(track.id)}
- >
-
-
-
-
-
-
-
-
- {track.annotations.length} 个关键帧
-
-
- ))}
- {tracks.length === 0 && (
-
- 暂无轨迹
-
- )}
-
-
-
-
-
-
- {/* Video Player and Canvas */}
-
- {/* Video Container */}
-
-
-
-
- {/* Video Info Overlay */}
-
- {currentVideo.name} | {formatTime(currentTime)} /{" "}
- {formatTime(duration)}
-
-
- {/* Tool Info Overlay */}
-
- {selectedTool === "select"
- ? "选择模式"
- : selectedTool === "rectangle"
- ? "矩形标注"
- : "点标注"}{" "}
- | {selectedLabel.name}
-
-
-
- {/* Video Controls */}
-
- {/* Timeline */}
-
-
- {formatTime(currentTime)}
- {formatTime(duration)}
-
-
handleSeek(value[0])}
- className="w-full"
- />
-
-
- {/* Player Controls */}
-
-
-
-
-
-
- {/* Volume Control */}
-
-
-
-
-
-
-
-
-
- {playbackSpeed}x
- {tracks.length} 轨迹
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/DataAnnotation/AutoAnnotation/AutoAnnotation.tsx b/frontend/src/pages/DataAnnotation/AutoAnnotation/AutoAnnotation.tsx
index 06c30f28e..133195787 100644
--- a/frontend/src/pages/DataAnnotation/AutoAnnotation/AutoAnnotation.tsx
+++ b/frontend/src/pages/DataAnnotation/AutoAnnotation/AutoAnnotation.tsx
@@ -23,6 +23,7 @@ import {
import CreateAutoAnnotationDialog from "./components/CreateAutoAnnotationDialog";
import EditAutoAnnotationDatasetDialog from "./components/EditAutoAnnotationDatasetDialog";
import ImportFromLabelStudioDialog from "./components/ImportFromLabelStudioDialog";
+import { useTranslation } from "react-i18next";
const STATUS_COLORS: Record = {
pending: "default",
@@ -32,23 +33,8 @@ const STATUS_COLORS: Record = {
cancelled: "default",
};
-const STATUS_LABELS: Record = {
- pending: "等待中",
- running: "处理中",
- completed: "已完成",
- failed: "失败",
- cancelled: "已取消",
-};
-
-const MODEL_SIZE_LABELS: Record = {
- n: "YOLOv8n (最快)",
- s: "YOLOv8s",
- m: "YOLOv8m",
- l: "YOLOv8l (推荐)",
- x: "YOLOv8x (最精确)",
-};
-
export default function AutoAnnotation() {
+ const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [tasks, setTasks] = useState([]);
const [showCreateDialog, setShowCreateDialog] = useState(false);
@@ -108,7 +94,7 @@ export default function AutoAnnotation() {
setTasks(response.data || response || []);
} catch (error) {
console.error("Failed to fetch auto annotation tasks:", error);
- if (!silent) message.error("获取任务列表失败");
+ if (!silent) message.error(t("dataAnnotation.autoAnnotation.messages.fetchTasksFailed"));
} finally {
if (!silent) setLoading(false);
}
@@ -126,25 +112,25 @@ export default function AutoAnnotation() {
const handleSyncToDatabase = (task: AutoAnnotationTask) => {
Modal.confirm({
- title: `确认将自动标注任务「${task.name}」在 Label Studio 中的标注结果同步到数据库吗?`,
+ title: t("dataAnnotation.autoAnnotation.messages.syncToDbTitle", { name: task.name }),
content: (
-
此操作会根据 Label Studio 中的任务数据覆盖当前文件标签与标注信息。
-
同步完成后,可在数据管理的文件详情中查看最新标签与标注。
+
{t("dataAnnotation.autoAnnotation.messages.syncToDbContent1")}
+
{t("dataAnnotation.autoAnnotation.messages.syncToDbContent2")}
),
- okText: "同步到数据库",
- cancelText: "取消",
+ okText: t("dataAnnotation.home.actions.syncToDb"),
+ cancelText: t("dataAnnotation.home.confirm.deleteCancelText"),
onOk: async () => {
- const hide = message.loading("正在从 Label Studio 同步标注到数据库...", 0);
+ const hide = message.loading(t("dataAnnotation.autoAnnotation.messages.syncLoading"), 0);
try {
await syncAutoAnnotationToDatabaseUsingPost(task.id);
hide();
- message.success("同步完成");
+ message.success(t("dataAnnotation.autoAnnotation.messages.syncSuccess"));
} catch (e) {
console.error(e);
hide();
- message.error("同步失败,请稍后重试");
+ message.error(t("dataAnnotation.autoAnnotation.messages.syncFailed"));
}
},
});
@@ -152,20 +138,20 @@ export default function AutoAnnotation() {
const handleDelete = (task: AutoAnnotationTask) => {
Modal.confirm({
- title: `确认删除自动标注任务「${task.name}」吗?`,
- content: "删除任务后,已生成的标注结果不会被删除。",
- okText: "删除",
+ title: t("dataAnnotation.autoAnnotation.confirm.deleteTitle", { name: task.name }),
+ content: t("dataAnnotation.autoAnnotation.confirm.deleteContent"),
+ okText: t("dataAnnotation.autoAnnotation.confirm.deleteOkText"),
okType: "danger",
- cancelText: "取消",
+ cancelText: t("dataAnnotation.autoAnnotation.confirm.deleteCancelText"),
onOk: async () => {
try {
await deleteAutoAnnotationTaskByIdUsingDelete(task.id);
- message.success("任务删除成功");
+ message.success(t("dataAnnotation.autoAnnotation.messages.deleteSuccess"));
fetchTasks();
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
} catch (error) {
console.error(error);
- message.error("删除失败,请稍后重试");
+ message.error(t("dataAnnotation.autoAnnotation.messages.deleteFailed"));
}
},
});
@@ -173,32 +159,32 @@ export default function AutoAnnotation() {
const handleDownload = async (task: AutoAnnotationTask) => {
try {
- message.loading("正在准备下载...", 0);
+ message.loading(t("dataAnnotation.autoAnnotation.messages.downloadPreparing"), 0);
await downloadAutoAnnotationResultUsingGet(task.id);
message.destroy();
- message.success("下载已开始");
+ message.success(t("dataAnnotation.autoAnnotation.messages.downloadStarted"));
} catch (error) {
console.error(error);
message.destroy();
- message.error("下载失败");
+ message.error(t("dataAnnotation.autoAnnotation.messages.downloadFailed"));
}
};
const handleAnnotate = (task: AutoAnnotationTask) => {
const datasetId = task.datasetId;
if (!datasetId) {
- message.error("该任务未绑定数据集,无法跳转 Label Studio");
+ message.error(t("dataAnnotation.autoAnnotation.messages.noDatasetBound"));
return;
}
const projId = datasetProjectMap[String(datasetId)];
if (!projId) {
- message.error("未找到对应的标注工程,请先为该数据集创建手动标注任务");
+ message.error(t("dataAnnotation.autoAnnotation.messages.noProjectFound"));
return;
}
if (!labelStudioBase) {
- message.error("无法跳转到 Label Studio:未配置 Label Studio 基础 URL");
+ message.error(t("dataAnnotation.autoAnnotation.messages.cannotJumpNoBase"));
return;
}
@@ -209,13 +195,13 @@ export default function AutoAnnotation() {
const handleViewResult = (task: AutoAnnotationTask) => {
if (task.outputPath) {
Modal.info({
- title: "标注结果路径",
+ title: t("dataAnnotation.autoAnnotation.messages.resultPathTitle"),
content: (
-
输出路径:{task.outputPath}
-
检测对象数:{task.detectedObjects}
+
{t("dataAnnotation.autoAnnotation.messages.outputPath")}: {task.outputPath}
+
{t("dataAnnotation.autoAnnotation.columns.detectedObjects")}: {task.detectedObjects}
- 处理图片数:{task.processedImages} / {task.totalImages}
+ {t("dataAnnotation.autoAnnotation.messages.processedImages")}: {task.processedImages} / {task.totalImages}
),
@@ -224,9 +210,9 @@ export default function AutoAnnotation() {
};
const columns: ColumnType[] = [
- { title: "任务名称", dataIndex: "name", key: "name", width: 200 },
+ { title: t("dataAnnotation.autoAnnotation.columns.name"), dataIndex: "name", key: "name", width: 200 },
{
- title: "数据集",
+ title: t("dataAnnotation.autoAnnotation.columns.dataset"),
dataIndex: "datasetName",
key: "datasetName",
width: 220,
@@ -249,47 +235,47 @@ export default function AutoAnnotation() {
},
},
{
- title: "模型",
+ title: t("dataAnnotation.autoAnnotation.columns.modelSize"),
dataIndex: ["config", "modelSize"],
key: "modelSize",
width: 120,
- render: (size: string) => MODEL_SIZE_LABELS[size] || size,
+ render: (size: string) => t(`dataAnnotation.autoAnnotation.modelSizeLabels.${size}`) || size,
},
{
- title: "置信度",
+ title: t("dataAnnotation.autoAnnotation.columns.confThreshold"),
dataIndex: ["config", "confThreshold"],
key: "confThreshold",
width: 100,
render: (threshold: number) => `${(threshold * 100).toFixed(0)}%`,
},
{
- title: "目标类别",
+ title: t("dataAnnotation.autoAnnotation.columns.targetClasses"),
dataIndex: ["config", "targetClasses"],
key: "targetClasses",
width: 120,
render: (classes: number[]) => (
0 ? classes.join(", ") : "全部类别"}
+ title={classes.length > 0 ? classes.join(", ") : t("dataAnnotation.home.allCategories")}
>
{classes.length > 0
- ? `${classes.length} 个类别`
- : "全部类别"}
+ ? t("dataAnnotation.home.categoriesCount", { count: classes.length })
+ : t("dataAnnotation.home.allCategories")}
),
},
{
- title: "状态",
+ title: t("dataAnnotation.autoAnnotation.columns.status"),
dataIndex: "status",
key: "status",
width: 100,
render: (status: AutoAnnotationStatus) => (
- {STATUS_LABELS[status]}
+ {t(`dataAnnotation.autoAnnotation.statusLabels.${status}`)}
),
},
{
- title: "进度",
+ title: t("dataAnnotation.autoAnnotation.columns.progress"),
dataIndex: "progress",
key: "progress",
width: 150,
@@ -303,52 +289,50 @@ export default function AutoAnnotation() {
),
},
{
- title: "检测对象数",
+ title: t("dataAnnotation.autoAnnotation.columns.detectedObjects"),
dataIndex: "detectedObjects",
key: "detectedObjects",
width: 100,
render: (count: number) => count.toLocaleString(),
},
{
- title: "创建时间",
+ title: t("dataAnnotation.autoAnnotation.columns.createdAt"),
dataIndex: "createdAt",
key: "createdAt",
width: 150,
render: (time: string) => new Date(time).toLocaleString(),
},
{
- title: "操作",
+ title: t("dataAnnotation.autoAnnotation.columns.actions"),
key: "actions",
width: 300,
fixed: "right",
render: (_: any, record: AutoAnnotationTask) => (
- {/* 一级功能:编辑(跳转 Label Studio) + 同步(导回结果) */}
-
+
}
onClick={() => handleAnnotate(record)}
>
- 编辑
+ {t("dataAnnotation.home.actions.edit")}
-
+
}
onClick={() => handleSyncToDatabase(record)}
>
- 同步到数据库
+ {t("dataAnnotation.autoAnnotation.actions.syncToDb")}
- {/* 已完成任务的查看/下载结果仍保留 */}
{record.status === "completed" && (
<>
-
+
-
+
)}
- {/* 二级功能菜单:折叠的删除任务 + 编辑任务数据集 */}
,
onClick: () => handleImportFromLabelStudio(record),
},
{
key: "edit-dataset",
- label: "编辑任务数据集",
+ label: t("dataAnnotation.autoAnnotation.actions.editDataset"),
icon: ,
onClick: () => handleEditTaskDataset(record),
},
{
key: "delete",
- label: "删除任务",
+ label: t("dataAnnotation.autoAnnotation.actions.delete"),
icon: ,
danger: true,
onClick: () => handleDelete(record),
@@ -394,9 +377,8 @@ export default function AutoAnnotation() {
}}
trigger={["click"]}
>
- }
- >
- 更多
+ }>
+ {t("dataAnnotation.autoAnnotation.more")}
@@ -407,7 +389,7 @@ export default function AutoAnnotation() {
return (
}
onClick={() => setShowCreateDialog(true)}
>
- 创建任务
+ {t("dataAnnotation.autoAnnotation.createTask")}
}
loading={loading}
onClick={() => fetchTasks()}
>
- 刷新
+ {t("dataAnnotation.autoAnnotation.refresh")}
}
diff --git a/frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx b/frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx
index 2eb3d2686..3f3b1cd9c 100644
--- a/frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx
+++ b/frontend/src/pages/DataAnnotation/AutoAnnotation/components/CreateAutoAnnotationDialog.tsx
@@ -136,7 +136,7 @@ export default function CreateAutoAnnotationDialog({
setDatasets(imageDatasets);
} catch (error) {
console.error("Failed to fetch datasets:", error);
- message.error("获取数据集列表失败");
+ message.error(t("dataAnnotation.autoAnnotation.messages.fetchTasksFailed"));
}
};
@@ -154,7 +154,7 @@ export default function CreateAutoAnnotationDialog({
const values = await form.validateFields();
if (imageFileCount === 0) {
- message.error("请至少选择一个图像文件");
+ message.error(t("dataAnnotation.create.messages.selectAtLeastOneImageFile"));
return;
}
@@ -193,12 +193,12 @@ export default function CreateAutoAnnotationDialog({
};
await createAutoAnnotationTaskUsingPost(payload);
- message.success("自动标注任务创建成功");
+ message.success(t("dataAnnotation.create.messages.autoCreateSuccess"));
onSuccess();
} catch (error: any) {
if (error.errorFields) return;
console.error("Failed to create auto annotation task:", error);
- message.error(error.message || "创建任务失败");
+ message.error(error.message || t("dataAnnotation.create.messages.autoCreateFailed"));
} finally {
setLoading(false);
}
@@ -213,7 +213,7 @@ export default function CreateAutoAnnotationDialog({
return (
-
+
-
+
{selectedDataset && (
- 当前数据集:{selectedDataset.name} - 已选择
- {imageFileCount} 个图像文件
+ {t("dataAnnotation.create.form.currentDatasetImages", { name: selectedDataset.name, count: imageFileCount })}
)}
-
+
-
+
`${(v || 0) * 100}%` }} />
-
+
handleClassSelectionChange(e.target.checked)}>
- 选中所有类别
+ {t("dataAnnotation.create.form.selectAllClasses")}
{!selectAllClasses && (
-
@@ -143,31 +150,31 @@ export default function AnnotationTaskCreate() {
layout="vertical"
>
{/* 基本信息 */}
-
基本信息
+ {t('dataAnnotation.create.basicInfo')}
-
+
-
+
({
label: (
@@ -177,7 +184,7 @@ export default function AnnotationTaskCreate() {
{dataset.name}
- {dataset?.fileCount} 文件 • {dataset.size}
+ {dataset?.fileCount} {t('common.table.file')} • {dataset.size}
),
@@ -188,11 +195,11 @@ export default function AnnotationTaskCreate() {
{/* 模板选择 */}
- 模板选择
+ {t('dataAnnotation.create.templateSelection')}
{/* Category Sidebar */}
@@ -218,7 +225,7 @@ export default function AnnotationTaskCreate() {
}
style={{ textAlign: "left", marginBottom: 8 }}
>
- {category}
+ {getCategoryLabel(category)}
);
})}
@@ -228,7 +235,7 @@ export default function AnnotationTaskCreate() {
icon={
}
onClick={() => setShowCustomTemplateDialog(true)}
>
- 自定义模板
+ {t('dataAnnotation.create.customTemplate')}
@@ -289,7 +296,7 @@ export default function AnnotationTaskCreate() {
- 自定义模板
+ {t('dataAnnotation.create.customTemplate')}
{selectedTemplate?.isCustom && (
@@ -297,7 +304,7 @@ export default function AnnotationTaskCreate() {
)}
- 创建符合特定需求的标注模板
+ {t('dataAnnotation.create.customTemplateDesc')}
@@ -312,7 +319,7 @@ export default function AnnotationTaskCreate() {
className="text-sm font-medium"
style={{ color: "#1677ff" }}
>
- 已选择模板
+ {t('dataAnnotation.create.selectedTemplate')}
-
+
diff --git a/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx b/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx
index 1eda1c1c9..286b957c9 100644
--- a/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx
+++ b/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx
@@ -142,7 +142,7 @@ export default function CreateAnnotationTask({
});
// The API returns: {code, message, data: {content, total, page, ...}}
- if (templateResponse.code === 200 && templateResponse.data) {
+ if (templateResponse.data) {
const fetchedTemplates = templateResponse.data.content || [];
console.log("Fetched templates:", fetchedTemplates);
setTemplates(fetchedTemplates);
@@ -268,7 +268,7 @@ export default function CreateAnnotationTask({
const selectedFiles = Object.values(selectedFilesMap) as any[];
if (selectedFiles.length === 0) {
- message?.error?.("请至少选择一个文件");
+ message?.error?.(t('dataAnnotation.create.messages.selectAtLeastOneFile'));
setSubmitting(false);
return;
}
@@ -293,12 +293,12 @@ export default function CreateAnnotationTask({
};
await createAnnotationTaskUsingPost(requestData);
- message?.success?.("创建标注任务成功");
+ message?.success?.(t('dataAnnotation.create.messages.createSuccess'));
onClose();
onRefresh();
} catch (err: any) {
console.error("Create annotation task failed", err);
- const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
+ const msg = err?.message || err?.data?.message || t('dataAnnotation.create.messages.createFailed');
(message as any)?.error?.(msg);
} finally {
setSubmitting(false);
@@ -310,7 +310,7 @@ export default function CreateAnnotationTask({
const values = await autoForm.validateFields();
if (imageFileCount === 0) {
- message.error("请至少选择一个图像文件");
+ message.error(t('dataAnnotation.create.messages.selectAtLeastOneImageFile'));
return;
}
@@ -351,14 +351,14 @@ export default function CreateAnnotationTask({
};
await createAutoAnnotationTaskUsingPost(payload);
- message.success("自动标注任务创建成功");
+ message.success(t('dataAnnotation.create.messages.autoCreateSuccess'));
// 触发上层刷新自动标注任务列表
(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 || "创建自动标注任务失败");
+ message.error(error.message || t('dataAnnotation.create.messages.autoCreateFailed'));
} finally {
setSubmitting(false);
}
@@ -375,18 +375,18 @@ export default function CreateAnnotationTask({
>
}
@@ -398,12 +398,12 @@ export default function CreateAnnotationTask({
items={[
{
key: "manual",
- label: "手动标注",
+ label: t('dataAnnotation.create.manual'),
children: (
{
const trimmed = (value || "").trim();
if (!trimmed) {
- return Promise.reject(new Error("请输入任务名称"));
+ return Promise.reject(new Error(t('dataAnnotation.create.form.nameRequired')));
}
if (trimmed.length < 3) {
return Promise.reject(
- new Error("任务名称至少需要 3 个字符(不含首尾空格,Label Studio 限制)"),
+ new Error(t('dataAnnotation.create.form.nameMinLength')),
);
}
return Promise.resolve();
@@ -424,22 +424,22 @@ export default function CreateAnnotationTask({
]}
>
setNameManuallyEdited(true)}
/>
{/* 第二行:先选模板,再选数据集,模板的数据类型驱动可选数据集类型 */}
{
const tplType = mapTemplateDataTypeToDatasetType(template.dataType);
@@ -467,7 +467,7 @@ export default function CreateAnnotationTask({
setSelectedDataset(null);
setSelectedFilesMap({});
manualForm.setFieldsValue({ datasetId: "" });
- message.warning("已根据模板类型筛选数据集,请重新选择数据集和文件");
+ message.warning(t('dataAnnotation.create.messages.datasetTypeFiltered'));
}
}}
optionRender={(option) => (
@@ -484,7 +484,7 @@ export default function CreateAnnotationTask({
{/* 选择数据集和文件(仅允许单一数据集,多文件),需先选模板再操作 */}
-
+
{selectedDataset && (
- 当前数据集:{selectedDataset.name} - 已选择
- {Object.keys(selectedFilesMap).length} 个文件
+ {t('dataAnnotation.create.form.currentDataset', { name: selectedDataset.name, count: Object.keys(selectedFilesMap).length })}
)}
@@ -520,35 +519,35 @@ export default function CreateAnnotationTask({
{/* 描述变为可选 */}
-
-
+
+
),
},
{
key: "auto",
- label: "自动标注",
+ label: t('dataAnnotation.create.auto'),
children: (
-
+
-
+
{selectedDataset && (
- 当前数据集:{selectedDataset.name} - 已选择
- {imageFileCount} 个图像文件
+ {t('dataAnnotation.create.form.currentDataset', { name: selectedDataset.name, count: imageFileCount })}
)}
@@ -572,30 +570,30 @@ export default function CreateAnnotationTask({
-
-
-
-
-
+
+
+
+
+
-
+
handleClassSelectionChange(e.target.checked)}
>
- 选中所有类别
+ {t('dataAnnotation.create.form.selectAllClasses')}
{!selectAllClasses && (
-
+
{COCO_CLASSES.map((cls) => (