feat: data annotation page adaptation to backend API. Improve labeling project creation module.

* feat: data annotation page adaptation to the backend API.

* feat: Implement labeling configuration editor and enhance annotation task creation form
This commit is contained in:
Jason Wang
2025-10-31 15:56:29 +08:00
committed by GitHub
parent 452d279195
commit ba0c69086a
20 changed files with 737 additions and 194 deletions

View File

@@ -1,14 +1,12 @@
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
import {
datasetTypeMap,
mapDataset,
} from "@/pages/DataManagement/dataset.const";
import { Button, Form, Input, Modal, Select } from "antd";
import { mapDataset } from "@/pages/DataManagement/dataset.const";
import { Button, Form, Input, Modal, Select, message } from "antd";
import TextArea from "antd/es/input/TextArea";
import { Database } from "lucide-react";
import { useEffect, useState } from "react";
import { createAnnotationTaskUsingPost } from "../../annotation.api";
import { Dataset } from "@/pages/DataManagement/dataset.model";
import LabelingConfigEditor from "./LabelingConfigEditor";
import { useRef } from "react";
export default function CreateAnnotationTask({
open,
@@ -21,6 +19,10 @@ export default function CreateAnnotationTask({
}) {
const [form] = Form.useForm();
const [datasets, setDatasets] = useState<Dataset[]>([]);
const [submitting, setSubmitting] = useState(false);
const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
const editorRef = useRef<any>(null);
const EDITOR_LIST_HEIGHT = 420;
useEffect(() => {
if (!open) return;
@@ -34,11 +36,36 @@ export default function CreateAnnotationTask({
fetchDatasets();
}, [open]);
// Reset form and manual-edit flag when modal opens
useEffect(() => {
if (open) {
form.resetFields();
setNameManuallyEdited(false);
}
}, [open, form]);
const handleSubmit = async () => {
const values = await form.validateFields();
await createAnnotationTaskUsingPost(values);
onClose();
onRefresh();
try {
const values = await form.validateFields();
setSubmitting(true);
await createAnnotationTaskUsingPost(values);
message?.success?.("创建标注任务成功");
onClose();
onRefresh();
} catch (err: any) {
console.error("Create annotation task failed", err);
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
// show a user friendly message
(message as any)?.error?.(msg);
} finally {
setSubmitting(false);
}
};
// Placeholder function: generates labeling interface from config
// For now it simply returns the parsed config (per requirement)
const generateLabelingInterface = (config: any) => {
return config;
};
return (
@@ -48,51 +75,124 @@ export default function CreateAnnotationTask({
title="创建标注任务"
footer={
<>
<Button onClick={onClose}></Button>
<Button type="primary" onClick={handleSubmit}>
<Button onClick={onClose} disabled={submitting}>
</Button>
<Button type="primary" onClick={handleSubmit} loading={submitting}>
</Button>
</>
}
width={1200}
>
<Form layout="vertical">
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: "请输入任务名称" }]}
>
<Input placeholder="输入任务名称" />
</Form.Item>
<Form.Item
label="描述"
name="description"
rules={[{ required: true, message: "请输入任务描述" }]}
>
<TextArea placeholder="详细描述标注任务的要求和目标" rows={3} />
</Form.Item>
<Form.Item
label="数据集"
name="datasetId"
rules={[{ required: true, message: "请选择数据集" }]}
>
<Select
placeholder="请选择数据集"
options={datasets.map((dataset) => {
return {
label: (
<div className="flex items-center justify-between gap-3 py-2">
<div className="flex items-center font-sm text-gray-900">
<span className="mr-2">{dataset.icon}</span>
<span>{dataset.name}</span>
<Form form={form} layout="vertical">
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
<div className="grid grid-cols-2 gap-4">
<Form.Item
label="数据集"
name="datasetId"
rules={[{ required: true, message: "请选择数据集" }]}
>
<Select
placeholder="请选择数据集"
options={datasets.map((dataset) => {
return {
label: (
<div className="flex items-center justify-between gap-3 py-2">
<div className="flex items-center font-sm text-gray-900">
<span className="mr-2">{(dataset as any).icon}</span>
<span>{dataset.name}</span>
</div>
<div className="text-xs text-gray-500">{dataset.size}</div>
</div>
<div className="text-xs text-gray-500">{dataset.size}</div>
</div>
),
value: dataset.id,
};
})}
/>
),
value: dataset.id,
};
})}
onChange={(value) => {
// 如果用户未手动修改名称,则用数据集名称作为默认任务名
if (!nameManuallyEdited) {
const ds = datasets.find((d) => d.id === value);
if (ds) {
form.setFieldsValue({ name: ds.name });
}
}
}}
/>
</Form.Item>
<Form.Item
label="标注工程名称"
name="name"
rules={[{ required: true, message: "请输入任务名称" }]}
>
<Input
placeholder="输入标注工程名称"
onChange={() => setNameManuallyEdited(true)}
/>
</Form.Item>
</div>
{/* 描述变为可选 */}
<Form.Item label="描述" name="description">
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} />
</Form.Item>
{/* 标注页面设计 模块:左侧为配置编辑,右侧为预览(作为表单的一部分,与其他字段同级) */}
<div style={{ marginTop: 8 }}>
<label className="block font-medium mb-2"></label>
<div style={{ display: "grid", gridTemplateColumns: "minmax(360px, 1fr) 1fr", gridTemplateRows: "auto 1fr", gap: 16 }}>
{/* Row 1: buttons on the left, spacer on the right so preview aligns with editor below */}
<div style={{ gridColumn: 1, gridRow: 1, display: 'flex', gap: 8 }}>
<Button onClick={() => editorRef.current?.addLabel?.()}></Button>
<Button type="primary" onClick={() => editorRef.current?.generate?.()}></Button>
</div>
{/* empty spacer to occupy top-right cell so preview starts on the second row */}
<div style={{ gridColumn: 2, gridRow: 1 }} />
{/* Row 2, Col 1: 编辑列表(固定高度) */}
<div style={{ gridColumn: 1, gridRow: 2, height: EDITOR_LIST_HEIGHT, overflowY: 'auto', paddingRight: 8, border: '1px solid #e6e6e6', borderRadius: 6, padding: 12 }}>
<LabelingConfigEditor
ref={editorRef}
hideFooter={true}
initial={undefined}
onGenerate={(config: any) => {
form.setFieldsValue({ labelingConfig: JSON.stringify(config, null, 2), labelingInterface: JSON.stringify(generateLabelingInterface(config), null, 2) });
}}
/>
<Form.Item
name="labelingConfig"
rules={[
{
validator: async (_, value) => {
if (!value || value === "") return Promise.resolve();
try {
JSON.parse(value);
return Promise.resolve();
} catch (e) {
return Promise.reject(new Error("请输入有效的 JSON"));
}
},
},
]}
style={{ display: "none" }}
>
<Input />
</Form.Item>
</div>
{/* Row 2, Col 2: 预览,与编辑列表在同一行,保持一致高度 */}
<div style={{ gridColumn: 2, gridRow: 2, display: 'flex', flexDirection: 'column' }}>
<Form.Item name="labelingInterface" style={{ flex: 1 }}>
<TextArea
placeholder="标注页面设计(只读,由标注配置生成)"
disabled
style={{ height: EDITOR_LIST_HEIGHT, resize: 'none' }}
/>
</Form.Item>
</div>
</div>
</div>
</Form>
</Modal>
);