feat: add labeling template. refactor: switch to Poetry, build and deploy of backend Python (#79)

* 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

* feat: Add tag configuration management and related components

- Introduced new components for tag selection and browsing in the frontend.
- Added API endpoint to fetch tag configuration from the backend.
- Implemented tag configuration management in the backend, including loading from YAML.
- Enhanced template service to support dynamic tag rendering based on configuration.
- Updated validation utilities to incorporate tag configuration checks.
- Refactored existing code to utilize the new tag configuration structure.

* feat: Refactor LabelStudioTagConfig for improved configuration loading and validation

* feat: Update Makefile to include backend-python-docker-build in the build process

* feat: Migrate to poetry for better deps management

* Add pyyaml dependency and update Dockerfile to use Poetry for dependency management

- Added pyyaml (>=6.0.3,<7.0.0) to pyproject.toml dependencies.
- Updated Dockerfile to install Poetry and manage dependencies using it.
- Improved layer caching by copying only dependency files before the application code.
- Removed unnecessary installation of build dependencies to keep the final image size small.

* feat: Remove duplicated backend-python-docker-build target from Makefile

* fix: airflow is not ready for adding yet

* feat: update Python version to 3.12 and remove project installation step in Dockerfile
This commit is contained in:
Jason Wang
2025-11-13 15:32:30 +08:00
committed by GitHub
parent 2660845b74
commit 45743f39f5
40 changed files with 3223 additions and 262 deletions

View File

@@ -110,7 +110,7 @@ export default function useFetchData<T>(
status: getFirstOfArray(filter?.status) || undefined,
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
page: current - pageOffset,
size: pageSize,
pageSize: pageSize, // Use camelCase for HTTP query params
}),
...additionalPollingFuncs.map((func) => func()),
];

View File

@@ -0,0 +1,67 @@
import { useState, useEffect } from "react";
import { message } from "antd";
import { getTagConfigUsingGet } from "../pages/DataAnnotation/annotation.api";
import type { LabelStudioTagConfig } from "../pages/DataAnnotation/annotation.tagconfig";
import { parseTagConfig, type TagOption } from "../pages/DataAnnotation/annotation.tagconfig";
interface UseTagConfigReturn {
config: LabelStudioTagConfig | null;
objectOptions: TagOption[];
controlOptions: TagOption[];
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
}
/**
* Hook to fetch and manage Label Studio tag configuration
* @param includeLabelingOnly - If true, only include controls with category="labeling" (default: true)
*/
export function useTagConfig(includeLabelingOnly: boolean = true): UseTagConfigReturn {
const [config, setConfig] = useState<LabelStudioTagConfig | null>(null);
const [objectOptions, setObjectOptions] = useState<TagOption[]>([]);
const [controlOptions, setControlOptions] = useState<TagOption[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchConfig = async () => {
setLoading(true);
setError(null);
try {
const response = await getTagConfigUsingGet();
if (response.code === 200 && response.data) {
const tagConfig: LabelStudioTagConfig = response.data;
setConfig(tagConfig);
const { objectOptions: objects, controlOptions: controls } =
parseTagConfig(tagConfig, includeLabelingOnly);
setObjectOptions(objects);
setControlOptions(controls);
} else {
const errorMsg = response.message || "获取标签配置失败";
setError(errorMsg);
message.error(errorMsg);
}
} catch (err: any) {
const errorMsg = err.message || "加载标签配置时出错";
setError(errorMsg);
console.error("Failed to fetch tag config:", err);
message.error(errorMsg);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchConfig();
}, []);
return {
config,
objectOptions,
controlOptions,
loading,
error,
refetch: fetchConfig,
};
}

View File

@@ -29,16 +29,16 @@ export default function CreateAnnotationTask({
// Fetch datasets
const { data: datasetData } = await queryDatasetsUsingGet({
page: 0,
size: 1000,
pageSize: 1000, // Use camelCase for HTTP params
});
setDatasets(datasetData.content.map(mapDataset) || []);
// Fetch templates
const templateResponse = await queryAnnotationTemplatesUsingGet({
page: 1,
size: 100, // Backend max is 100
size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize')
});
// The API returns: {code, message, data: {content, total, page, ...}}
if (templateResponse.code === 200 && templateResponse.data) {
const fetchedTemplates = templateResponse.data.content || [];
@@ -68,7 +68,6 @@ export default function CreateAnnotationTask({
try {
const values = await form.validateFields();
setSubmitting(true);
// Send templateId instead of labelingConfig
const requestData = {
name: values.name,
@@ -76,7 +75,6 @@ export default function CreateAnnotationTask({
datasetId: values.datasetId,
templateId: values.templateId,
};
await createAnnotationTaskUsingPost(requestData);
message?.success?.("创建标注任务成功");
onClose();
@@ -154,7 +152,6 @@ export default function CreateAnnotationTask({
/>
</Form.Item>
</div>
{/* 描述变为可选 */}
<Form.Item label="描述" name="description">
<TextArea placeholder="(可选)详细描述标注任务的要求和目标" rows={3} />

View File

@@ -29,14 +29,14 @@ export default function CreateAnnotationTask({
// Fetch datasets
const { data: datasetData } = await queryDatasetsUsingGet({
page: 0,
size: 1000,
pageSize: 1000, // Use camelCase for HTTP params
});
setDatasets(datasetData.content.map(mapDataset) || []);
// Fetch templates
const templateResponse = await queryAnnotationTemplatesUsingGet({
page: 1,
size: 100, // Backend max is 100
size: 100, // Backend max is 100 (template API uses 'size' not 'pageSize')
});
// The API returns: {code, message, data: {content, total, page, ...}}

View File

@@ -111,7 +111,7 @@ export default function DataAnnotation() {
cancelText: "取消",
onOk: async () => {
try {
await deleteAnnotationTaskByIdUsingDelete({ m: task.id, proj: task.labelingProjId });
await deleteAnnotationTaskByIdUsingDelete(task.id);
message.success("映射删除成功");
fetchData();
// clear selection if deleted item was selected
@@ -198,7 +198,7 @@ export default function DataAnnotation() {
onOk: async () => {
try {
await Promise.all(
selectedRows.map((r) => deleteAnnotationTaskByIdUsingDelete({ m: r.id, proj: r.labelingProjId }))
selectedRows.map((r) => deleteAnnotationTaskByIdUsingDelete(r.id))
);
message.success("批量删除已完成");
fetchData();

View File

@@ -17,6 +17,7 @@ import {
updateAnnotationTemplateByIdUsingPut,
} from "../annotation.api";
import type { AnnotationTemplate } from "../annotation.model";
import TagSelector from "./components/TagSelector";
const { TextArea } = Input;
const { Option } = Select;
@@ -111,22 +112,6 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
}
};
const controlTypes = [
{ value: "Choices", label: "选项 (单选/多选)" },
{ value: "RectangleLabels", label: "矩形框 (目标检测)" },
{ value: "PolygonLabels", label: "多边形" },
{ value: "Labels", label: "标签 (文本高亮)" },
{ value: "TextArea", label: "文本区域" },
{ value: "Rating", label: "评分" },
];
const objectTypes = [
{ value: "Image", label: "图像" },
{ value: "Text", label: "文本" },
{ value: "Audio", label: "音频" },
{ value: "Video", label: "视频" },
];
const needsOptions = (type: string) => {
return ["Choices", "RectangleLabels", "PolygonLabels", "Labels"].includes(type);
};
@@ -243,13 +228,7 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0, width: 150 }}
>
<Select>
{objectTypes.map((t) => (
<Option key={t.value} value={t.value}>
{t.label}
</Option>
))}
</Select>
<TagSelector type="object" />
</Form.Item>
<Form.Item
@@ -356,13 +335,7 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
rules={[{ required: true, message: "必填" }]}
style={{ marginBottom: 0 }}
>
<Select placeholder="选择控件类型">
{controlTypes.map((t) => (
<Option key={t.value} value={t.value}>
{t.label}
</Option>
))}
</Select>
<TagSelector type="control" />
</Form.Item>
<Form.Item

View File

@@ -0,0 +1,161 @@
import React, { useState } from "react";
import {
Card,
Button,
Space,
Row,
Col,
Drawer,
Typography,
message,
} from "antd";
import {
PlusOutlined,
EyeOutlined,
CodeOutlined,
AppstoreOutlined,
} from "@ant-design/icons";
import { TagBrowser } from "./components";
const { Paragraph } = Typography;
interface VisualTemplateBuilderProps {
onSave?: (templateCode: string) => void;
}
/**
* Visual Template Builder
* Provides a drag-and-drop interface for building Label Studio templates
*/
const VisualTemplateBuilder: React.FC<VisualTemplateBuilderProps> = ({
onSave,
}) => {
const [drawerVisible, setDrawerVisible] = useState(false);
const [previewVisible, setPreviewVisible] = useState(false);
const [selectedTags, setSelectedTags] = useState<
Array<{ name: string; category: "object" | "control" }>
>([]);
const handleTagSelect = (tagName: string, category: "object" | "control") => {
message.info(`选择了 ${category === "object" ? "对象" : "控件"}: ${tagName}`);
setSelectedTags([...selectedTags, { name: tagName, category }]);
setDrawerVisible(false);
};
const handleSave = () => {
// TODO: Generate template XML from selectedTags
message.success("模板保存成功");
onSave?.("<View><!-- Generated template --></View>");
};
return (
<div style={{ padding: "24px" }}>
<Row gutter={16}>
<Col span={24}>
<Card
title="可视化模板构建器"
extra={
<Space>
<Button
icon={<AppstoreOutlined />}
onClick={() => setDrawerVisible(true)}
>
</Button>
<Button
icon={<CodeOutlined />}
onClick={() => setPreviewVisible(true)}
>
</Button>
<Button
type="primary"
icon={<EyeOutlined />}
onClick={handleSave}
>
</Button>
</Space>
}
>
<div
style={{
minHeight: "400px",
border: "2px dashed #d9d9d9",
borderRadius: "8px",
padding: "24px",
textAlign: "center",
}}
>
{selectedTags.length === 0 ? (
<div>
<Paragraph type="secondary">
"浏览标签"
</Paragraph>
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => setDrawerVisible(true)}
>
</Button>
</div>
) : (
<Space direction="vertical" size="large">
{selectedTags.map((tag, index) => (
<Card key={index} size="small">
<div>
{tag.category === "object" ? "对象" : "控件"}: {tag.name}
</div>
</Card>
))}
</Space>
)}
</div>
</Card>
</Col>
</Row>
<Drawer
title="标签浏览器"
placement="right"
width={800}
open={drawerVisible}
onClose={() => setDrawerVisible(false)}
>
<TagBrowser onTagSelect={handleTagSelect} />
</Drawer>
<Drawer
title="模板代码预览"
placement="right"
width={600}
open={previewVisible}
onClose={() => setPreviewVisible(false)}
>
<pre
style={{
background: "#f5f5f5",
padding: "16px",
borderRadius: "4px",
overflow: "auto",
}}
>
<code>
{`<View>
<!-- 根据选择的标签生成的模板代码 -->
${selectedTags
.map(
(tag) =>
`<${tag.name}${tag.category === "object" ? ' name="obj" value="$data"' : ' name="ctrl" toName="obj"'} />`
)
.join("\n ")}
</View>`}
</code>
</pre>
</Drawer>
</div>
);
};
export default VisualTemplateBuilder;

View File

@@ -0,0 +1,260 @@
import React from "react";
import { Card, Tabs, List, Tag, Typography, Space, Empty, Spin } from "antd";
import {
AppstoreOutlined,
ControlOutlined,
InfoCircleOutlined,
} from "@ant-design/icons";
import { useTagConfig } from "../../../../hooks/useTagConfig";
import {
getControlDisplayName,
getObjectDisplayName,
getControlGroups,
} from "../../annotation.tagconfig";
import type { TagOption } from "../../annotation.tagconfig";
const { Title, Paragraph, Text } = Typography;
interface TagBrowserProps {
onTagSelect?: (tagName: string, category: "object" | "control") => void;
}
/**
* Tag Browser Component
* Displays all available Label Studio tags in a browsable interface
*/
const TagBrowser: React.FC<TagBrowserProps> = ({ onTagSelect }) => {
const { config, objectOptions, controlOptions, loading, error } =
useTagConfig();
if (loading) {
return (
<Card>
<div style={{ textAlign: "center", padding: "40px" }}>
<Spin size="large" />
<div style={{ marginTop: 16 }}>...</div>
</div>
</Card>
);
}
if (error) {
return (
<Card>
<Empty
description={
<div>
<div>{error}</div>
<Text type="secondary"></Text>
</div>
}
/>
</Card>
);
}
const renderObjectList = () => (
<List
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
dataSource={objectOptions}
renderItem={(item: TagOption) => {
const objConfig = config?.objects[item.value];
return (
<List.Item>
<Card
hoverable
size="small"
onClick={() => onTagSelect?.(item.value, "object")}
style={{ height: "100%" }}
>
<Space direction="vertical" size="small" style={{ width: "100%" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text strong>{getObjectDisplayName(item.value)}</Text>
<Tag color="blue">&lt;{item.value}&gt;</Tag>
</div>
<Text type="secondary" style={{ fontSize: 12 }}>
{item.description}
</Text>
{objConfig && (
<div style={{ marginTop: 8 }}>
<Text style={{ fontSize: 11, color: "#8c8c8c" }}>
:{" "}
{objConfig.required_attrs.join(", ") || "无"}
</Text>
</div>
)}
</Space>
</Card>
</List.Item>
);
}}
/>
);
const renderControlsByGroup = () => {
const groups = getControlGroups();
return (
<Tabs
defaultActiveKey="classification"
items={Object.entries(groups).map(([groupKey, groupConfig]) => {
const groupControls = controlOptions.filter((opt: TagOption) =>
groupConfig.controls.includes(opt.value)
);
return {
key: groupKey,
label: groupConfig.label,
children: (
<List
grid={{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4 }}
dataSource={groupControls}
locale={{ emptyText: "此分组暂无控件" }}
renderItem={(item: TagOption) => {
const ctrlConfig = config?.controls[item.value];
return (
<List.Item>
<Card
hoverable
size="small"
onClick={() => onTagSelect?.(item.value, "control")}
style={{ height: "100%" }}
>
<Space direction="vertical" size="small" style={{ width: "100%" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Text strong>
{getControlDisplayName(item.value)}
</Text>
<Tag color="green">&lt;{item.value}&gt;</Tag>
</div>
<Text type="secondary" style={{ fontSize: 12 }}>
{item.description}
</Text>
{ctrlConfig && (
<Space
size={4}
wrap
style={{ marginTop: 8 }}
>
{ctrlConfig.requires_children && (
<Tag
color="orange"
style={{ fontSize: 10, margin: 0 }}
>
&lt;{ctrlConfig.child_tag}&gt;
</Tag>
)}
{ctrlConfig.required_attrs.includes("toName") && (
<Tag
color="purple"
style={{ fontSize: 10, margin: 0 }}
>
</Tag>
)}
</Space>
)}
</Space>
</Card>
</List.Item>
);
}}
/>
),
};
})}
/>
);
};
return (
<Card>
<Tabs
defaultActiveKey="controls"
items={[
{
key: "controls",
label: (
<span>
<ControlOutlined />
({controlOptions.length})
</span>
),
children: renderControlsByGroup(),
},
{
key: "objects",
label: (
<span>
<AppstoreOutlined />
({objectOptions.length})
</span>
),
children: renderObjectList(),
},
{
key: "help",
label: (
<span>
<InfoCircleOutlined />
使
</span>
),
children: (
<div style={{ padding: "16px" }}>
<Title level={4}>Label Studio </Title>
<Paragraph>
</Paragraph>
<ul>
<li>
<Text strong></Text>
</li>
<li>
<Text strong></Text>
</li>
</ul>
<Title level={5} style={{ marginTop: 24 }}>
</Title>
<Paragraph>
<pre style={{ background: "#f5f5f5", padding: 12, borderRadius: 4 }}>
{`<View>
<!-- 数据对象 -->
<Image name="image" value="$image" />
<!-- 控件 -->
<RectangleLabels name="label" toName="image">
<Label value="人物" />
<Label value="车辆" />
</RectangleLabels>
</View>`}
</pre>
</Paragraph>
<Title level={5} style={{ marginTop: 24 }}>
</Title>
<ul>
<li>
<Text code>name</Text>
</li>
<li>
<Text code>toName</Text> name
</li>
<li>
<Text code>value</Text> $ $image, $text
</li>
<li>
<Text code>required</Text>
</li>
</ul>
</div>
),
},
]}
/>
</Card>
);
};
export default TagBrowser;

View File

@@ -0,0 +1,301 @@
import React, { useState, useEffect } from "react";
import { Select, Tooltip, Spin, Collapse, Tag, Space } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { getTagConfigUsingGet } from "../../annotation.api";
import type {
LabelStudioTagConfig,
TagOption,
} from "../../annotation.tagconfig";
import {
parseTagConfig,
getControlDisplayName,
getObjectDisplayName,
getControlGroups,
} from "../../annotation.tagconfig";
const { Option, OptGroup } = Select;
interface TagSelectorProps {
value?: string;
onChange?: (value: string) => void;
type: "object" | "control";
placeholder?: string;
style?: React.CSSProperties;
disabled?: boolean;
}
/**
* Tag Selector Component
* Dynamically fetches and displays available Label Studio tags from backend config
*/
const TagSelector: React.FC<TagSelectorProps> = ({
value,
onChange,
type,
placeholder,
style,
disabled,
}) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [tagOptions, setTagOptions] = useState<TagOption[]>([]);
useEffect(() => {
fetchTagConfig();
}, []);
const fetchTagConfig = async () => {
setLoading(true);
setError(null);
try {
const response = await getTagConfigUsingGet();
if (response.code === 200 && response.data) {
const config: LabelStudioTagConfig = response.data;
const { objectOptions, controlOptions } = parseTagConfig(config);
if (type === "object") {
setTagOptions(objectOptions);
} else {
setTagOptions(controlOptions);
}
} else {
setError(response.message || "获取标签配置失败");
}
} catch (err: any) {
console.error("Failed to fetch tag config:", err);
setError("加载标签配置时出错");
} finally {
setLoading(false);
}
};
if (loading) {
return (
<Select
placeholder="加载中..."
style={style}
disabled
suffixIcon={<Spin size="small" />}
/>
);
}
if (error) {
return (
<Tooltip title={error}>
<Select
placeholder="加载失败,点击重试"
style={style}
disabled={disabled}
status="error"
onClick={() => fetchTagConfig()}
/>
</Tooltip>
);
}
// Group controls by usage pattern
if (type === "control") {
const groups = getControlGroups();
const groupedOptions: Record<string, TagOption[]> = {};
const ungroupedOptions: TagOption[] = [];
// Group the controls
Object.entries(groups).forEach(([groupKey, groupConfig]) => {
groupedOptions[groupKey] = tagOptions.filter((opt) =>
groupConfig.controls.includes(opt.value)
);
});
// Find ungrouped controls
const allGroupedControls = new Set(
Object.values(groups).flatMap((g) => g.controls)
);
tagOptions.forEach((opt) => {
if (!allGroupedControls.has(opt.value)) {
ungroupedOptions.push(opt);
}
});
return (
<Select
value={value}
onChange={onChange}
placeholder={placeholder || "选择控件类型"}
style={style}
disabled={disabled}
showSearch
optionFilterProp="label"
>
{Object.entries(groups).map(([groupKey, groupConfig]) => {
const options = groupedOptions[groupKey];
if (options.length === 0) return null;
return (
<OptGroup key={groupKey} label={groupConfig.label}>
{options.map((opt) => (
<Option key={opt.value} value={opt.value} label={opt.label}>
<div className="flex items-center justify-between">
<span>{getControlDisplayName(opt.value)}</span>
<Tooltip title={opt.description}>
<InfoCircleOutlined
style={{ color: "#8c8c8c", fontSize: 12 }}
/>
</Tooltip>
</div>
</Option>
))}
</OptGroup>
);
})}
{ungroupedOptions.length > 0 && (
<OptGroup label="其他">
{ungroupedOptions.map((opt) => (
<Option key={opt.value} value={opt.value} label={opt.label}>
<div className="flex items-center justify-between">
<span>{getControlDisplayName(opt.value)}</span>
<Tooltip title={opt.description}>
<InfoCircleOutlined
style={{ color: "#8c8c8c", fontSize: 12 }}
/>
</Tooltip>
</div>
</Option>
))}
</OptGroup>
)}
</Select>
);
}
// Objects selector (no grouping)
return (
<Select
value={value}
onChange={onChange}
placeholder={placeholder || "选择数据对象类型"}
style={style}
disabled={disabled}
showSearch
optionFilterProp="label"
>
{tagOptions.map((opt) => (
<Option key={opt.value} value={opt.value} label={opt.label}>
<div className="flex items-center justify-between">
<span>{getObjectDisplayName(opt.value)}</span>
<Tooltip title={opt.description}>
<InfoCircleOutlined style={{ color: "#8c8c8c", fontSize: 12 }} />
</Tooltip>
</div>
</Option>
))}
</Select>
);
};
export default TagSelector;
/**
* Tag Info Panel Component
* Displays detailed information about a selected tag
*/
interface TagInfoPanelProps {
tagConfig: LabelStudioTagConfig | null;
tagType: string;
category: "object" | "control";
}
export const TagInfoPanel: React.FC<TagInfoPanelProps> = ({
tagConfig,
tagType,
category,
}) => {
if (!tagConfig || !tagType) {
return null;
}
const config =
category === "object"
? tagConfig.objects[tagType]
: tagConfig.controls[tagType];
if (!config) {
return null;
}
return (
<Collapse
size="small"
items={[
{
key: "1",
label: "标签配置详情",
children: (
<Space direction="vertical" size="small" style={{ width: "100%" }}>
<div>
<strong></strong>
{config.description}
</div>
<div>
<strong></strong>
<div style={{ marginTop: 4 }}>
{config.required_attrs.map((attr: string) => (
<Tag key={attr} color="red">
{attr}
</Tag>
))}
</div>
</div>
{config.optional_attrs &&
Object.keys(config.optional_attrs).length > 0 && (
<div>
<strong></strong>
<div style={{ marginTop: 4 }}>
{Object.entries(config.optional_attrs).map(
([attrName, attrConfig]: [string, any]) => (
<Tooltip
key={attrName}
title={
<div>
{attrConfig.description && (
<div>{attrConfig.description}</div>
)}
{attrConfig.type && (
<div>: {attrConfig.type}</div>
)}
{attrConfig.default !== undefined && (
<div>: {String(attrConfig.default)}</div>
)}
{attrConfig.values && (
<div>
: {attrConfig.values.join(", ")}
</div>
)}
</div>
}
>
<Tag color="blue" style={{ cursor: "help" }}>
{attrName}
</Tag>
</Tooltip>
)
)}
</div>
</div>
)}
{config.requires_children && (
<div>
<strong></strong>
<Tag color="green"> &lt;{config.child_tag}&gt;</Tag>
</div>
)}
</Space>
),
},
]}
/>
);
};

View File

@@ -0,0 +1,3 @@
export { default as TagSelector } from "./TagSelector";
export { default as TagBrowser } from "./TagBrowser";
export { TagInfoPanel } from "./TagSelector";

View File

@@ -1,3 +1,4 @@
export { default as TemplateList } from "./TemplateList";
export { default as TemplateForm } from "./TemplateForm";
export { default as TemplateDetail } from "./TemplateDetail";
export { TagBrowser, TagSelector, TagInfoPanel } from "./components";

View File

@@ -26,17 +26,9 @@ export function queryAnnotationTaskByIdUsingGet(mappingId: string | number) {
export function queryMappingsBySourceUsingGet(datasetId: string, params?: any) {
return get(`/api/annotation/project/by-source/${datasetId}`, params);
}
export function deleteAnnotationTaskByIdUsingDelete(params?: any) {
// Ensure query params are sent in the URL for backend endpoints that expect Query parameters
if (params && typeof params === "object" && !Array.isArray(params)) {
const pairs = Object.keys(params)
.filter((k) => params[k] !== undefined && params[k] !== null)
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`);
const query = pairs.length ? `?${pairs.join("&")}` : "";
return del(`/api/annotation/project${query}`);
}
return del(`/api/annotation/project`, params);
export function deleteAnnotationTaskByIdUsingDelete(mappingId: string) {
// Backend expects mapping UUID as path parameter
return del(`/api/annotation/project/${mappingId}`);
}
// 智能预标注相关接口
@@ -100,32 +92,37 @@ export function getAnnotationStatisticsUsingGet(params?: any) {
return get("/api/v1/annotation/statistics", params);
}
// 标签配置管理
export function getTagConfigUsingGet() {
return get("/api/annotation/tags/config");
}
// 标注模板管理
export function queryAnnotationTemplatesUsingGet(params?: any) {
return get("/api/annotation/templates", params);
return get("/api/annotation/template", params);
}
export function createAnnotationTemplateUsingPost(data: any) {
return post("/api/annotation/templates", data);
return post("/api/annotation/template", data);
}
export function queryAnnotationTemplateByIdUsingGet(
templateId: string | number
) {
return get(`/api/annotation/templates/${templateId}`);
return get(`/api/v1/annotation/templates/${templateId}`);
}
export function updateAnnotationTemplateByIdUsingPut(
templateId: string | number,
data: any
) {
return put(`/api/annotation/templates/${templateId}`, data);
return put(`/api/v1/annotation/templates/${templateId}`, data);
}
export function deleteAnnotationTemplateByIdUsingDelete(
templateId: string | number
) {
return del(`/api/annotation/templates/${templateId}`);
return del(`/api/v1/annotation/templates/${templateId}`);
}
// 主动学习相关接口

View File

@@ -0,0 +1,187 @@
/**
* Label Studio Tag Configuration Types
* Corresponds to runtime/datamate-python/app/module/annotation/config/label_studio_tags.yaml
*/
export interface TagAttributeConfig {
type?: "boolean" | "number" | "string";
values?: string[];
default?: any;
description?: string;
}
export interface TagConfig {
description: string;
required_attrs: string[];
optional_attrs?: Record<string, TagAttributeConfig>;
requires_children?: boolean;
child_tag?: string;
child_required_attrs?: string[];
category?: string; // e.g., "labeling" or "layout" for controls; "image", "text", etc. for objects
}
export interface LabelStudioTagConfig {
objects: Record<string, TagConfig>;
controls: Record<string, TagConfig>;
}
/**
* UI-friendly representation of a tag for selection
*/
export interface TagOption {
value: string;
label: string;
description: string;
category: "object" | "control";
requiresChildren: boolean;
childTag?: string;
requiredAttrs: string[];
optionalAttrs?: Record<string, TagAttributeConfig>;
}
/**
* Convert backend tag config to frontend tag options
* @param config - The full tag configuration from backend
* @param includeLabelingOnly - If true, only include controls with category="labeling" (default: true)
*/
export function parseTagConfig(
config: LabelStudioTagConfig,
includeLabelingOnly: boolean = true
): {
objectOptions: TagOption[];
controlOptions: TagOption[];
} {
const objectOptions: TagOption[] = Object.entries(config.objects).map(
([key, value]) => ({
value: key,
label: key,
description: value.description,
category: "object" as const,
requiresChildren: value.requires_children || false,
childTag: value.child_tag,
requiredAttrs: value.required_attrs,
optionalAttrs: value.optional_attrs,
})
);
const controlOptions: TagOption[] = Object.entries(config.controls)
.filter(([_, value]) => {
// If includeLabelingOnly is true, filter out layout controls
if (includeLabelingOnly) {
return value.category === "labeling";
}
return true;
})
.map(([key, value]) => ({
value: key,
label: key,
description: value.description,
category: "control" as const,
requiresChildren: value.requires_children || false,
childTag: value.child_tag,
requiredAttrs: value.required_attrs,
optionalAttrs: value.optional_attrs,
}));
return { objectOptions, controlOptions };
}
/**
* Get user-friendly display name for control types
*/
export function getControlDisplayName(controlType: string): string {
const displayNames: Record<string, string> = {
Choices: "选项 (单选/多选)",
RectangleLabels: "矩形框",
PolygonLabels: "多边形",
Labels: "标签",
TextArea: "文本区域",
Rating: "评分",
Taxonomy: "分类树",
Ranker: "排序",
List: "列表",
BrushLabels: "画笔分割",
EllipseLabels: "椭圆",
KeyPointLabels: "关键点",
Rectangle: "矩形",
Polygon: "多边形",
Ellipse: "椭圆",
KeyPoint: "关键点",
Brush: "画笔",
Number: "数字输入",
DateTime: "日期时间",
Relation: "关系",
Relations: "关系组",
Pairwise: "成对比较",
};
return displayNames[controlType] || controlType;
}
/**
* Get user-friendly display name for object types
*/
export function getObjectDisplayName(objectType: string): string {
const displayNames: Record<string, string> = {
Image: "图像",
Text: "文本",
Audio: "音频",
Video: "视频",
HyperText: "HTML内容",
PDF: "PDF文档",
Markdown: "Markdown内容",
Paragraphs: "段落",
Table: "表格",
AudioPlus: "高级音频",
Timeseries: "时间序列",
Vector: "向量数据",
Chat: "对话数据",
};
return displayNames[objectType] || objectType;
}
/**
* Group control types by common usage patterns
*/
export function getControlGroups(): Record<
string,
{ label: string; controls: string[] }
> {
return {
classification: {
label: "分类标注",
controls: ["Choices", "Taxonomy", "Labels", "Rating"],
},
detection: {
label: "目标检测",
controls: [
"RectangleLabels",
"PolygonLabels",
"EllipseLabels",
"KeyPointLabels",
"Rectangle",
"Polygon",
"Ellipse",
"KeyPoint",
],
},
segmentation: {
label: "分割标注",
controls: ["BrushLabels", "Brush", "BitmaskLabels", "MagicWand"],
},
text: {
label: "文本输入",
controls: ["TextArea", "Number", "DateTime"],
},
other: {
label: "其他",
controls: [
"TimeseriesLabels",
"VectorLabels",
"ParagraphLabels",
"VideoRectangle",
],
},
};
}