Skip to content

Commit c5ccc56

Browse files
authored
feat: Add labeling template (#72)
* feat: Enhance annotation module with template management and validation - Added DatasetMappingCreateRequest and DatasetMappingUpdateRequest schemas to handle dataset mapping requests with camelCase and snake_case support. - Introduced Annotation Template schemas including CreateAnnotationTemplateRequest, UpdateAnnotationTemplateRequest, and AnnotationTemplateResponse for managing annotation templates. - Implemented AnnotationTemplateService for creating, updating, retrieving, and deleting annotation templates, including validation of configurations and XML generation. - Added utility class LabelStudioConfigValidator for validating Label Studio configurations and XML formats. - Updated database schema for annotation templates and labeling projects to include new fields and constraints. - Seeded initial annotation templates for various use cases including image classification, object detection, and text classification. * feat: Enhance TemplateForm with improved validation and dynamic field rendering; update LabelStudio config validation for camelCase support * feat: Update docker-compose.yml to mark datamate dataset volume and network as external
1 parent 451d3c8 commit c5ccc56

File tree

24 files changed

+2794
-253
lines changed

24 files changed

+2794
-253
lines changed

deployment/docker/label-studio/docker-compose.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ volumes:
5252
label-studio-db:
5353
dataset_volume:
5454
name: datamate-dataset-volume
55+
external: true
5556

5657
networks:
5758
datamate:
5859
driver: bridge
59-
name: datamate-network
60+
name: datamate-network
61+
external: true
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
2+
import { mapDataset } from "@/pages/DataManagement/dataset.const";
3+
import { Button, Form, Input, Modal, Select, message } from "antd";
4+
import TextArea from "antd/es/input/TextArea";
5+
import { useEffect, useState } from "react";
6+
import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet } from "../../annotation.api";
7+
import { Dataset } from "@/pages/DataManagement/dataset.model";
8+
import type { AnnotationTemplate } from "../../annotation.model";
9+
10+
export default function CreateAnnotationTask({
11+
open,
12+
onClose,
13+
onRefresh,
14+
}: {
15+
open: boolean;
16+
onClose: () => void;
17+
onRefresh: () => void;
18+
}) {
19+
const [form] = Form.useForm();
20+
const [datasets, setDatasets] = useState<Dataset[]>([]);
21+
const [templates, setTemplates] = useState<AnnotationTemplate[]>([]);
22+
const [submitting, setSubmitting] = useState(false);
23+
const [nameManuallyEdited, setNameManuallyEdited] = useState(false);
24+
25+
useEffect(() => {
26+
if (!open) return;
27+
const fetchData = async () => {
28+
try {
29+
// Fetch datasets
30+
const { data: datasetData } = await queryDatasetsUsingGet({
31+
page: 0,
32+
size: 1000,
33+
});
34+
setDatasets(datasetData.content.map(mapDataset) || []);
35+
36+
// Fetch templates
37+
const templateResponse = await queryAnnotationTemplatesUsingGet({
38+
page: 1,
39+
size: 100, // Backend max is 100
40+
});
41+
42+
// The API returns: {code, message, data: {content, total, page, ...}}
43+
if (templateResponse.code === 200 && templateResponse.data) {
44+
const fetchedTemplates = templateResponse.data.content || [];
45+
console.log("Fetched templates:", fetchedTemplates);
46+
setTemplates(fetchedTemplates);
47+
} else {
48+
console.error("Failed to fetch templates:", templateResponse);
49+
setTemplates([]);
50+
}
51+
} catch (error) {
52+
console.error("Error fetching data:", error);
53+
setTemplates([]);
54+
}
55+
};
56+
fetchData();
57+
}, [open]);
58+
59+
// Reset form and manual-edit flag when modal opens
60+
useEffect(() => {
61+
if (open) {
62+
form.resetFields();
63+
setNameManuallyEdited(false);
64+
}
65+
}, [open, form]);
66+
67+
const handleSubmit = async () => {
68+
try {
69+
const values = await form.validateFields();
70+
setSubmitting(true);
71+
72+
// Send templateId instead of labelingConfig
73+
const requestData = {
74+
name: values.name,
75+
description: values.description,
76+
datasetId: values.datasetId,
77+
templateId: values.templateId,
78+
};
79+
80+
await createAnnotationTaskUsingPost(requestData);
81+
message?.success?.("创建标注任务成功");
82+
onClose();
83+
onRefresh();
84+
} catch (err: any) {
85+
console.error("Create annotation task failed", err);
86+
const msg = err?.message || err?.data?.message || "创建失败,请稍后重试";
87+
(message as any)?.error?.(msg);
88+
} finally {
89+
setSubmitting(false);
90+
}
91+
};
92+
93+
return (
94+
<Modal
95+
open={open}
96+
onCancel={onClose}
97+
title="创建标注任务"
98+
footer={
99+
<>
100+
<Button onClick={onClose} disabled={submitting}>
101+
取消
102+
</Button>
103+
<Button type="primary" onClick={handleSubmit} loading={submitting}>
104+
确定
105+
</Button>
106+
</>
107+
}
108+
width={800}
109+
>
110+
<Form form={form} layout="vertical">
111+
{/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
112+
<div className="grid grid-cols-2 gap-4">
113+
<Form.Item
114+
label="数据集"
115+
name="datasetId"
116+
rules={[{ required: true, message: "请选择数据集" }]}
117+
>
118+
<Select
119+
placeholder="请选择数据集"
120+
options={datasets.map((dataset) => {
121+
return {
122+
label: (
123+
<div className="flex items-center justify-between gap-3 py-2">
124+
<div className="flex items-center font-sm text-gray-900">
125+
<span className="mr-2">{(dataset as any).icon}</span>
126+
<span>{dataset.name}</span>
127+
</div>
128+
<div className="text-xs text-gray-500">{dataset.size}</div>
129+
</div>
130+
),
131+
value: dataset.id,
132+
};
133+
})}
134+
onChange={(value) => {
135+
// 如果用户未手动修改名称,则用数据集名称作为默认任务名
136+
if (!nameManuallyEdited) {
137+
const ds = datasets.find((d) => d.id === value);
138+
if (ds) {
139+
form.setFieldsValue({ name: ds.name });
140+
}
141+
}
142+
}}
143+
/>
144+
</Form.Item>
145+
146+
<Form.Item
147+
label="标注工程名称"
148+
name="name"
149+
rules={[{ required: true, message: "请输入任务名称" }]}
150+
>
151+
<Input
152+
placeholder="输入标注工程名称"
153+
onChange={() => setNameManuallyEdited(true)}
154+
/>
155+
</Form.Item>
156+
</div>
157+
158+
{/* 描述变为可选 */}
159+
<Form.Item label="描述" name="description">
160+
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} />
161+
</Form.Item>
162+
163+
{/* 标注模板选择 */}
164+
<Form.Item
165+
label="标注模板"
166+
name="templateId"
167+
rules={[{ required: true, message: "请选择标注模板" }]}
168+
>
169+
<Select
170+
placeholder={templates.length === 0 ? "暂无可用模板,请先创建模板" : "请选择标注模板"}
171+
showSearch
172+
optionFilterProp="label"
173+
notFoundContent={templates.length === 0 ? "暂无模板,请前往「标注模板」页面创建" : "未找到匹配的模板"}
174+
options={templates.map((template) => ({
175+
label: template.name,
176+
value: template.id,
177+
// Add description as subtitle
178+
title: template.description,
179+
}))}
180+
optionRender={(option) => (
181+
<div>
182+
<div style={{ fontWeight: 500 }}>{option.label}</div>
183+
{option.data.title && (
184+
<div style={{ fontSize: 12, color: '#999', marginTop: 2 }}>
185+
{option.data.title}
186+
</div>
187+
)}
188+
</div>
189+
)}
190+
/>
191+
</Form.Item>
192+
</Form>
193+
</Modal>
194+
);
195+
}

0 commit comments

Comments
 (0)