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 { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
import { import { mapDataset } from "@/pages/DataManagement/dataset.const";
datasetTypeMap, import { Button, Form, Input, Modal, Select, message } from "antd";
mapDataset,
} from "@/pages/DataManagement/dataset.const";
import { Button, Form, Input, Modal, Select } from "antd";
import TextArea from "antd/es/input/TextArea"; import TextArea from "antd/es/input/TextArea";
import { Database } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { createAnnotationTaskUsingPost } from "../../annotation.api"; import { createAnnotationTaskUsingPost } from "../../annotation.api";
import { Dataset } from "@/pages/DataManagement/dataset.model"; import { Dataset } from "@/pages/DataManagement/dataset.model";
import LabelingConfigEditor from "./LabelingConfigEditor";
import { useRef } from "react";
export default function CreateAnnotationTask({ export default function CreateAnnotationTask({
open, open,
@@ -21,6 +19,10 @@ export default function CreateAnnotationTask({
}) { }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [datasets, setDatasets] = useState<Dataset[]>([]); 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(() => { useEffect(() => {
if (!open) return; if (!open) return;
@@ -34,11 +36,36 @@ export default function CreateAnnotationTask({
fetchDatasets(); fetchDatasets();
}, [open]); }, [open]);
// Reset form and manual-edit flag when modal opens
useEffect(() => {
if (open) {
form.resetFields();
setNameManuallyEdited(false);
}
}, [open, form]);
const handleSubmit = async () => { const handleSubmit = async () => {
const values = await form.validateFields(); try {
await createAnnotationTaskUsingPost(values); const values = await form.validateFields();
onClose(); setSubmitting(true);
onRefresh(); 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 ( return (
@@ -48,51 +75,124 @@ export default function CreateAnnotationTask({
title="创建标注任务" title="创建标注任务"
footer={ footer={
<> <>
<Button onClick={onClose}></Button> <Button onClick={onClose} disabled={submitting}>
<Button type="primary" onClick={handleSubmit}>
</Button>
<Button type="primary" onClick={handleSubmit} loading={submitting}>
</Button> </Button>
</> </>
} }
width={1200}
> >
<Form layout="vertical"> <Form form={form} layout="vertical">
<Form.Item {/* 数据集 与 标注工程名称 并排显示(数据集在左) */}
label="名称" <div className="grid grid-cols-2 gap-4">
name="name" <Form.Item
rules={[{ required: true, message: "请输入任务名称" }]} label="数据集"
> name="datasetId"
<Input placeholder="输入任务名称" /> rules={[{ required: true, message: "请选择数据集" }]}
</Form.Item> >
<Form.Item <Select
label="描述" placeholder="请选择数据集"
name="description" options={datasets.map((dataset) => {
rules={[{ required: true, message: "请输入任务描述" }]} return {
> label: (
<TextArea placeholder="详细描述标注任务的要求和目标" rows={3} /> <div className="flex items-center justify-between gap-3 py-2">
</Form.Item> <div className="flex items-center font-sm text-gray-900">
<Form.Item <span className="mr-2">{(dataset as any).icon}</span>
label="数据集" <span>{dataset.name}</span>
name="datasetId" </div>
rules={[{ required: true, message: "请选择数据集" }]} <div className="text-xs text-gray-500">{dataset.size}</div>
>
<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>
</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> </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> </Form>
</Modal> </Modal>
); );

View File

@@ -0,0 +1,187 @@
import { Button, Card, Input, InputNumber, Popconfirm, Select, Switch, Tooltip } from "antd";
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
import { useState, useImperativeHandle, forwardRef } from "react";
type LabelType = "string" | "number" | "enum";
type LabelItem = {
id: string;
name: string;
type: LabelType;
required?: boolean;
// for enum: values; for number: min/max
values?: string[];
min?: number | null;
max?: number | null;
step?: number | null;
};
type LabelingConfigEditorProps = {
initial?: any;
onGenerate: (config: any) => void;
hideFooter?: boolean;
};
export default forwardRef<any, LabelingConfigEditorProps>(function LabelingConfigEditor(
{ initial, onGenerate, hideFooter }: LabelingConfigEditorProps,
ref: any
) {
const [labels, setLabels] = useState<LabelItem[]>(() => {
if (initial && initial.labels && Array.isArray(initial.labels)) {
return initial.labels.map((l: any, idx: number) => ({
id: `${Date.now()}-${idx}`,
name: l.name || "",
type: l.type || "string",
required: !!l.required,
values: l.values || (l.type === "enum" ? [] : undefined),
min: l.min ?? null,
max: l.max ?? null,
}));
}
return [];
});
const addLabel = () => {
setLabels((s) => [
...s,
{ id: `${Date.now()}-${Math.random()}`, name: "", type: "string", required: false, step: null },
]);
};
const updateLabel = (id: string, patch: Partial<LabelItem>) => {
setLabels((s) => s.map((l) => (l.id === id ? { ...l, ...patch } : l)));
};
const removeLabel = (id: string) => {
setLabels((s) => s.filter((l) => l.id !== id));
};
const generate = () => {
// basic validation: label name non-empty
for (const l of labels) {
if (!l.name || l.name.trim() === "") {
// focus not available here, just abort
// Could show a more friendly UI; keep simple for now
// eslint-disable-next-line no-alert
alert("请为所有标签填写名称");
return;
}
if (l.type === "enum") {
if (!l.values || l.values.length === 0) {
alert(`枚举标签 ${l.name} 需要至少一个取值`);
return;
}
}
if (l.type === "number") {
// validate min/max
if (l.min != null && l.max != null && l.min > l.max) {
alert(`数值标签 ${l.name} 的最小值不能大于最大值`);
return;
}
// validate step
if (l.step != null && (!(typeof l.step === "number") || l.step <= 0)) {
alert(`数值标签 ${l.name} 的步长必须为大于 0 的数值`);
return;
}
}
}
const config = {
labels: labels.map((l) => {
const item: any = { name: l.name, type: l.type, required: !!l.required };
if (l.type === "enum") item.values = l.values || [];
if (l.type === "number") {
if (l.min != null) item.min = l.min;
if (l.max != null) item.max = l.max;
}
return item;
}),
};
onGenerate(config);
};
useImperativeHandle(ref, () => ({
addLabel,
generate,
}));
return (
<div>
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{labels.map((label) => (
<Card size="small" key={label.id} styles={{ body: { padding: 10 } }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
<Input
placeholder="标签名称"
value={label.name}
onChange={(e) => updateLabel(label.id, { name: e.target.value })}
style={{ flex: "1 1 160px", minWidth: 120 }}
/>
<Select
value={label.type}
onChange={(v) => updateLabel(label.id, { type: v as LabelType })}
options={[{ label: "文本", value: "string" }, { label: "数值", value: "number" }, { label: "枚举", value: "enum" }]}
style={{ width: 120, flex: "0 0 120px" }}
/>
{label.type === "enum" && (
<Input.TextArea
placeholder="每行一个枚举值,按回车换行"
value={(label.values || []).join("\n")}
onChange={(e) => updateLabel(label.id, { values: e.target.value.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) })}
onKeyDown={(e) => {
// Prevent parent handlers (like Form submit or modal shortcuts) from intercepting Enter
e.stopPropagation();
}}
rows={3}
style={{ flex: "1 1 220px", minWidth: 160, width: "100%", resize: "vertical" }}
/>
)}
{label.type === "number" && (
<div style={{ display: "flex", gap: 8, alignItems: "center", flex: "0 0 auto" }}>
<Tooltip title="最小值">
<InputNumber value={label.min ?? null} onChange={(v) => updateLabel(label.id, { min: v ?? null })} placeholder="min" />
</Tooltip>
<Tooltip title="最大值">
<InputNumber value={label.max ?? null} onChange={(v) => updateLabel(label.id, { max: v ?? null })} placeholder="max" />
</Tooltip>
<Tooltip title="步长 (step)">
<InputNumber value={label.step ?? null} onChange={(v) => updateLabel(label.id, { step: v ?? null })} placeholder="step" min={0} />
</Tooltip>
</div>
)}
<div style={{ display: "flex", alignItems: "center", gap: 8, marginLeft: "auto" }}>
<span style={{ fontSize: 12, color: "rgba(0,0,0,0.65)" }}></span>
<Switch checked={!!label.required} onChange={(v) => updateLabel(label.id, { required: v })} />
<Popconfirm title="确认删除该标签?" onConfirm={() => removeLabel(label.id)}>
<Button type="text" icon={<DeleteOutlined />} />
</Popconfirm>
</div>
</div>
<div style={{ marginTop: 8, color: "rgba(0,0,0,0.45)", fontSize: 12 }}>
{label.type === "string" && <span></span>}
{label.type === "number" && <span> min / max / step</span>}
{label.type === "enum" && <span></span>}
</div>
</Card>
))}
{!hideFooter && (
<div style={{ display: "flex", gap: 8 }}>
<Button icon={<PlusOutlined />} onClick={addLabel}>
</Button>
<Button type="primary" onClick={generate}>
JSON
</Button>
</div>
)}
</div>
</div>
);
}
);

View File

@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { Card, Button, Table, message } from "antd"; import { Card, Button, Table, message, Modal } from "antd";
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
@@ -8,22 +8,22 @@ import {
} from "@ant-design/icons"; } from "@ant-design/icons";
import { SearchControls } from "@/components/SearchControls"; import { SearchControls } from "@/components/SearchControls";
import CardView from "@/components/CardView"; import CardView from "@/components/CardView";
import { useNavigate } from "react-router";
import type { AnnotationTask } from "../annotation.model"; import type { AnnotationTask } from "../annotation.model";
import useFetchData from "@/hooks/useFetchData"; import useFetchData from "@/hooks/useFetchData";
import { import {
deleteAnnotationTaskByIdUsingDelete, deleteAnnotationTaskByIdUsingDelete,
queryAnnotationTasksUsingGet, queryAnnotationTasksUsingGet,
syncAnnotationTaskUsingPost, syncAnnotationTaskUsingPost,
getConfigUsingGet,
} from "../annotation.api"; } from "../annotation.api";
import { mapAnnotationTask } from "../annotation.const"; import { mapAnnotationTask } from "../annotation.const";
import CreateAnnotationTask from "../Create/components/CreateAnnptationTaskDialog"; import CreateAnnotationTask from "../Create/components/CreateAnnptationTaskDialog";
import { ColumnType } from "antd/es/table"; import { ColumnType } from "antd/es/table";
import DevelopmentInProgress from "@/components/DevelopmentInProgress"; // Note: DevelopmentInProgress intentionally not used here
export default function DataAnnotation() { export default function DataAnnotation() {
return <DevelopmentInProgress showTime="2025.10.30" />; // return <DevelopmentInProgress showTime="2025.10.30" />;
const navigate = useNavigate(); // navigate not needed for label studio external redirect
const [viewMode, setViewMode] = useState<"list" | "card">("list"); const [viewMode, setViewMode] = useState<"list" | "card">("list");
const [showCreateDialog, setShowCreateDialog] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false);
@@ -35,22 +35,182 @@ export default function DataAnnotation() {
setSearchParams, setSearchParams,
fetchData, fetchData,
handleFiltersChange, handleFiltersChange,
} = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask); } = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0);
const [labelStudioBase, setLabelStudioBase] = useState<string | null>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]);
const [selectedRows, setSelectedRows] = useState<any[]>([]);
// prefetch config on mount so clicking annotate is fast and we know whether base URL exists
// useEffect ensures this runs once
useEffect(() => {
let mounted = true;
(async () => {
try {
const cfg = await getConfigUsingGet();
const url = cfg?.data?.labelStudioUrl || "";
if (mounted) setLabelStudioBase((url).replace(/\/+$/, "") || null);
} catch (e) {
if (mounted) setLabelStudioBase(null);
}
})();
return () => {
mounted = false;
};
}, []);
const handleAnnotate = (task: AnnotationTask) => { const handleAnnotate = (task: AnnotationTask) => {
navigate(`/data/annotation/task-annotate/${task.datasetType}/${task.id}`); // Open Label Studio project page in a new tab
(async () => {
try {
// prefer using labeling project id already present on the task
// `mapAnnotationTask` normalizes upstream fields into `labelingProjId`/`projId`,
// so prefer those and fall back to the task id if necessary.
let labelingProjId = (task as any).labelingProjId || (task as any).projId || undefined;
// no fallback external mapping lookup; rely on normalized fields from mapAnnotationTask
// use prefetched base if available
const base = labelStudioBase;
// no debug logging in production
if (labelingProjId) {
// only open external Label Studio when we have a configured base url
if (base) {
const target = `${base}/projects/${labelingProjId}/data`;
window.open(target, "_blank");
} else {
// no external Label Studio URL configured — do not perform internal redirect in this version
message.error("无法跳转到 Label Studio:未配置 Label Studio 基础 URL");
return;
}
} else {
// no labeling project id available — do not attempt internal redirect in this version
message.error("无法跳转到 Label Studio:该映射未绑定标注项目");
return;
}
} catch (error) {
// on error, surface a user-friendly message instead of redirecting
message.error("无法跳转到 Label Studio:发生错误,请检查配置或控制台日志");
return;
}
})();
}; };
const handleDelete = async (task: AnnotationTask) => { const handleDelete = (task: AnnotationTask) => {
await deleteAnnotationTaskByIdUsingDelete({ Modal.confirm({
m: task.id, title: `确认删除标注任务「${task.name}」吗?`,
proj: task.projId, content: (
<div>
<div></div>
<div></div>
</div>
),
okText: "删除",
okType: "danger",
cancelText: "取消",
onOk: async () => {
try {
await deleteAnnotationTaskByIdUsingDelete({ m: task.id, proj: task.labelingProjId });
message.success("映射删除成功");
fetchData();
// clear selection if deleted item was selected
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
setSelectedRows((rows) => rows.filter((r) => r.id !== task.id));
} catch (e) {
console.error(e);
message.error("删除失败,请稍后重试");
}
},
}); });
}; };
const handleSync = async (task: AnnotationTask, format: string) => { const handleSync = (task: AnnotationTask, batchSize: number = 50) => {
await syncAnnotationTaskUsingPost({ task, format }); Modal.confirm({
message.success("任务同步请求已发送"); title: `确认同步标注任务「${task.name}」吗?`,
content: (
<div>
<div></div>
<div></div>
</div>
),
okText: "同步",
cancelText: "取消",
onOk: async () => {
try {
await syncAnnotationTaskUsingPost({ id: task.id, batchSize });
message.success("任务同步请求已发送");
// optional: refresh list/status
fetchData();
// clear selection for the task
setSelectedRowKeys((keys) => keys.filter((k) => k !== task.id));
setSelectedRows((rows) => rows.filter((r) => r.id !== task.id));
} catch (e) {
console.error(e);
message.error("同步失败,请稍后重试");
}
},
});
};
const handleBatchSync = (batchSize: number = 50) => {
if (!selectedRows || selectedRows.length === 0) return;
Modal.confirm({
title: `确认同步所选 ${selectedRows.length} 个标注任务吗?`,
content: (
<div>
<div></div>
<div></div>
</div>
),
okText: "同步",
cancelText: "取消",
onOk: async () => {
try {
await Promise.all(
selectedRows.map((r) => syncAnnotationTaskUsingPost({ id: r.id, batchSize }))
);
message.success("批量同步请求已发送");
fetchData();
setSelectedRowKeys([]);
setSelectedRows([]);
} catch (e) {
console.error(e);
message.error("批量同步失败,请稍后重试");
}
},
});
};
const handleBatchDelete = () => {
if (!selectedRows || selectedRows.length === 0) return;
Modal.confirm({
title: `确认删除所选 ${selectedRows.length} 个标注任务吗?`,
content: (
<div>
<div></div>
<div></div>
</div>
),
okText: "删除",
okType: "danger",
cancelText: "取消",
onOk: async () => {
try {
await Promise.all(
selectedRows.map((r) => deleteAnnotationTaskByIdUsingDelete({ m: r.id, proj: r.labelingProjId }))
);
message.success("批量删除已完成");
fetchData();
setSelectedRowKeys([]);
setSelectedRows([]);
} catch (e) {
console.error(e);
message.error("批量删除失败,请稍后重试");
}
},
});
}; };
const operations = [ const operations = [
@@ -79,7 +239,7 @@ export default function DataAnnotation() {
}, },
]; ];
const columns: ColumnType[] = [ const columns: ColumnType<any>[] = [
{ {
title: "任务名称", title: "任务名称",
dataIndex: "name", dataIndex: "name",
@@ -115,14 +275,14 @@ export default function DataAnnotation() {
fixed: "right" as const, fixed: "right" as const,
width: 150, width: 150,
dataIndex: "actions", dataIndex: "actions",
render: (_: any, task: AnnotationTask) => ( render: (_: any, task: any) => (
<div className="flex items-center justify-center space-x-1"> <div className="flex items-center justify-center space-x-1">
{operations.map((operation) => ( {operations.map((operation) => (
<Button <Button
key={operation.key} key={operation.key}
type="text" type="text"
icon={operation.icon} icon={operation.icon}
onClick={() => operation?.onClick?.(task)} onClick={() => (operation?.onClick as any)?.(task)}
title={operation.label} title={operation.label}
/> />
))} ))}
@@ -136,13 +296,31 @@ export default function DataAnnotation() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-xl font-bold"></h1> <h1 className="text-xl font-bold"></h1>
<Button <div className="flex items-center space-x-2">
type="primary" {/* Batch action buttons - availability depends on selection count */}
icon={<PlusOutlined />} <div className="flex items-center space-x-1">
onClick={() => setShowCreateDialog(true)} <Button
> onClick={() => handleBatchSync(50)}
disabled={selectedRowKeys.length === 0}
</Button> >
</Button>
<Button
danger
onClick={handleBatchDelete}
disabled={selectedRowKeys.length === 0}
>
</Button>
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setShowCreateDialog(true)}
>
</Button>
</div>
</div> </div>
{/* Filters Toolbar */} {/* Filters Toolbar */}
@@ -163,15 +341,23 @@ export default function DataAnnotation() {
<Card> <Card>
<Table <Table
key="id" key="id"
rowKey="id"
loading={loading} loading={loading}
columns={columns} columns={columns}
dataSource={tableData} dataSource={tableData}
pagination={pagination} pagination={pagination}
rowSelection={{
selectedRowKeys,
onChange: (keys, rows) => {
setSelectedRowKeys(keys as (string | number)[]);
setSelectedRows(rows as any[]);
},
}}
scroll={{ x: "max-content", y: "calc(100vh - 20rem)" }} scroll={{ x: "max-content", y: "calc(100vh - 20rem)" }}
/> />
</Card> </Card>
) : ( ) : (
<CardView data={tableData} operations={operations} /> <CardView data={tableData} operations={operations as any} pagination={pagination} loading={loading} />
)} )}
<CreateAnnotationTask <CreateAnnotationTask
open={showCreateDialog} open={showCreateDialog}

View File

@@ -2,22 +2,41 @@ import { get, post, put, del, download } from "@/utils/request";
// 标注任务管理相关接口 // 标注任务管理相关接口
export function queryAnnotationTasksUsingGet(params?: any) { export function queryAnnotationTasksUsingGet(params?: any) {
return get("/project/mappings/list", params); return get("/api/annotation/project", params);
}
// 获取应用配置(包含 Label Studio 基础 URL)
export function getConfigUsingGet() {
return get("/api/annotation/about");
} }
export function createAnnotationTaskUsingPost(data: any) { export function createAnnotationTaskUsingPost(data: any) {
return post("/api/project/create", data); return post("/api/annotation/project", data);
} }
export function syncAnnotationTaskUsingPost(data: any) { export function syncAnnotationTaskUsingPost(data: any) {
return post(`/api/project/sync`, data); return post(`/api/annotation/task/sync`, data);
} }
export function queryAnnotationTaskByIdUsingGet(taskId: string | number) { export function queryAnnotationTaskByIdUsingGet(mappingId: string | number) {
return get(`/api/v1/annotation/tasks/${taskId}`); return get(`/api/annotation/project/${mappingId}`);
}
// 根据源 datasetId 查询映射关系(分页)
export function queryMappingsBySourceUsingGet(datasetId: string, params?: any) {
return get(`/api/annotation/project/by-source/${datasetId}`, params);
} }
export function deleteAnnotationTaskByIdUsingDelete(params?: any) { export function deleteAnnotationTaskByIdUsingDelete(params?: any) {
return del(`/api/project/mappings`, params); // 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);
} }
// 智能预标注相关接口 // 智能预标注相关接口

View File

@@ -1,5 +1,5 @@
import { StickyNote } from "lucide-react"; import { StickyNote } from "lucide-react";
import { AnnotationTask, AnnotationTaskStatus } from "./annotation.model"; import { AnnotationTaskStatus } from "./annotation.model";
import { import {
CheckCircleOutlined, CheckCircleOutlined,
ClockCircleOutlined, ClockCircleOutlined,
@@ -31,26 +31,41 @@ export const AnnotationTaskStatusMap = {
}, },
}; };
export function mapAnnotationTask(task: AnnotationTask) { export function mapAnnotationTask(task: any) {
// Normalize labeling project id from possible backend field names
const labelingProjId = task?.labelingProjId || task?.labelingProjectId || task?.projId || task?.labeling_project_id || "";
const statsArray = task?.statistics
? [
{ label: "准确率", value: task.statistics.accuracy ?? "-" },
{ label: "平均时长", value: task.statistics.averageTime ?? "-" },
{ label: "待复核", value: task.statistics.reviewCount ?? "-" },
]
: [];
return { return {
...task, ...task,
id: task.mapping_id, id: task.id,
projId: task.labelling_project_id, // provide consistent field for components
name: task.labelling_project_name, labelingProjId,
createdAt: task.created_at, projId: labelingProjId,
updatedAt: task.last_updated_at, name: task.name,
description: task.description || "",
createdAt: task.createdAt,
updatedAt: task.updatedAt,
icon: <StickyNote />, icon: <StickyNote />,
iconColor: "bg-blue-100", iconColor: "bg-blue-100",
status: { status: {
label: label:
task.status === "completed" task.status === "completed"
? "已完成" ? "已完成"
: task.status === "in_progress" : task.status === "processing"
? "进行中" ? "进行中"
: task.status === "skipped" : task.status === "skipped"
? "已跳过" ? "已跳过"
: "待开始", : "待开始",
color: "bg-blue-100", color: "bg-blue-100",
}, },
statistics: statsArray,
}; };
} }

View File

@@ -2,16 +2,20 @@ import type { DatasetType } from "@/pages/DataManagement/dataset.model";
export enum AnnotationTaskStatus { export enum AnnotationTaskStatus {
ACTIVE = "active", ACTIVE = "active",
PROCESSING = "processing",
INACTIVE = "inactive", INACTIVE = "inactive",
PROCESSING = "processing",
COMPLETED = "completed",
SKIPPED = "skipped",
} }
export interface AnnotationTask { export interface AnnotationTask {
id: string; id: string;
name: string; name: string;
annotationCount: number; labelingProjId: string;
createdAt: string;
datasetId: string; datasetId: string;
annotationCount: number;
description?: string; description?: string;
assignedTo?: string; assignedTo?: string;
progress: number; progress: number;
@@ -23,5 +27,7 @@ export interface AnnotationTask {
status: AnnotationTaskStatus; status: AnnotationTaskStatus;
totalDataCount: number; totalDataCount: number;
type: DatasetType; type: DatasetType;
createdAt: string;
updatedAt: string; updatedAt: string;
} }

View File

@@ -1,6 +1,5 @@
import { Button, Card, Badge, Breadcrumb } from "antd"; import { Button, Card, Badge, Breadcrumb } from "antd";
import { import {
ArrowLeft,
Download, Download,
Users, Users,
Scissors, Scissors,
@@ -16,10 +15,10 @@ import {
mockTasks, mockTasks,
presetEvaluationDimensions, presetEvaluationDimensions,
} from "@/mock/evaluation"; } from "@/mock/evaluation";
import { Link, useNavigate } from "react-router"; import { Link } from "react-router";
const EvaluationTaskReport = () => { const EvaluationTaskReport = () => {
const navigate = useNavigate(); // const navigate = useNavigate();
const selectedTask = mockTasks[0]; // 假设我们只展示第一个任务的报告 const selectedTask = mockTasks[0]; // 假设我们只展示第一个任务的报告
// 获取任务的所有维度 // 获取任务的所有维度
@@ -131,7 +130,7 @@ const EvaluationTaskReport = () => {
</span> </span>
} }
bodyStyle={{ paddingTop: 0 }} styles={{ body: { paddingTop: 0 } }}
> >
{/* 维度评分 */} {/* 维度评分 */}
<div className="mt-4"> <div className="mt-4">
@@ -198,7 +197,7 @@ const EvaluationTaskReport = () => {
</span> </span>
} }
bodyStyle={{ paddingTop: 0 }} styles={{ body: { paddingTop: 0 } }}
> >
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
<div className="grid grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-2 gap-4 text-sm">
@@ -257,7 +256,7 @@ const EvaluationTaskReport = () => {
QA对详情 QA对详情
</span> </span>
} }
bodyStyle={{ paddingTop: 0 }} styles={{ body: { paddingTop: 0 } }}
> >
<div className="space-y-4 mt-4"> <div className="space-y-4 mt-4">
{mockQAPairs.map((qa) => ( {mockQAPairs.map((qa) => (
@@ -282,11 +281,10 @@ const EvaluationTaskReport = () => {
{[1, 2, 3, 4, 5].map((star) => ( {[1, 2, 3, 4, 5].map((star) => (
<Star <Star
key={star} key={star}
className={`w-4 h-4 ${ className={`w-4 h-4 ${star <= qa.score
star <= qa.score
? "text-yellow-400" ? "text-yellow-400"
: "text-gray-300" : "text-gray-300"
}`} }`}
style={star <= qa.score ? { fill: "#facc15" } : {}} style={star <= qa.score ? { fill: "#facc15" } : {}}
/> />
))} ))}

View File

@@ -308,7 +308,7 @@ export default function WorkflowEditor({
className="cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow" className="cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow"
draggable draggable
onDragStart={(event) => onDragStart(event, nodeType.type)} onDragStart={(event) => onDragStart(event, nodeType.type)}
bodyStyle={{ padding: 16 }} styles={{ body: { padding: 16 } }}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0"> <div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
@@ -408,12 +408,12 @@ export default function WorkflowEditor({
nds.map((node) => nds.map((node) =>
node.id === selectedNode.id node.id === selectedNode.id
? { ? {
...node, ...node,
data: { data: {
...node.data, ...node.data,
name: e.target.value, name: e.target.value,
}, },
} }
: node : node
) )
); );
@@ -436,12 +436,12 @@ export default function WorkflowEditor({
nds.map((node) => nds.map((node) =>
node.id === selectedNode.id node.id === selectedNode.id
? { ? {
...node, ...node,
data: { data: {
...node.data, ...node.data,
description: e.target.value, description: e.target.value,
}, },
} }
: node : node
) )
); );

View File

@@ -54,12 +54,11 @@ const CustomNode = ({ data, selected }: { data: any; selected: boolean }) => {
/> />
<Card <Card
className={`w-80 transition-all duration-200 ${ className={`w-80 transition-all duration-200 ${selected
selected
? "ring-2 ring-blue-500 shadow-lg" ? "ring-2 ring-blue-500 shadow-lg"
: "shadow-md hover:shadow-lg" : "shadow-md hover:shadow-lg"
}`} }`}
bodyStyle={{ padding: 0 }} styles={{ body: { padding: 0 } }}
> >
<div className="pb-3 bg-blue-50 border-b px-4 pt-4"> <div className="pb-3 bg-blue-50 border-b px-4 pt-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

View File

@@ -99,19 +99,15 @@ class Client:
title: str, title: str,
description: str = "", description: str = "",
label_config: Optional[str] = None, label_config: Optional[str] = None,
data_type: str = "image"
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
"""创建Label Studio项目""" """创建Label Studio项目"""
try: try:
logger.debug(f"Creating Label Studio project: {title}") logger.debug(f"Creating Label Studio project: {title}")
if not label_config:
label_config = self.get_label_config_by_type(data_type)
project_data = { project_data = {
"title": title, "title": title,
"description": description, "description": description,
"label_config": label_config.strip() "label_config": label_config or "<View></View>"
} }
response = await self.client.post("/api/projects", json=project_data) response = await self.client.post("/api/projects", json=project_data)

View File

@@ -1,5 +1,6 @@
from fastapi import APIRouter from fastapi import APIRouter
from .about import router as about_router
from .project import router as project_router from .project import router as project_router
from .task import router as task_router from .task import router as task_router
@@ -8,5 +9,6 @@ router = APIRouter(
tags = ["annotation"] tags = ["annotation"]
) )
router.include_router(about_router)
router.include_router(project_router) router.include_router(project_router)
router.include_router(task_router) router.include_router(task_router)

View File

@@ -0,0 +1,35 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional
from app.db.session import get_db
from app.module.shared.schema import StandardResponse
from app.module.dataset import DatasetManagementService
from app.core.logging import get_logger
from app.core.config import settings
from app.exception import NoDatasetInfoFoundError, DatasetMappingNotFoundError
from ..client import LabelStudioClient
from ..service.sync import SyncService
from ..service.mapping import DatasetMappingService
from ..schema import (
ConfigResponse
)
router = APIRouter(
prefix="/about",
tags=["annotation/about"]
)
logger = get_logger(__name__)
@router.get("", response_model=StandardResponse[ConfigResponse])
async def get_config():
"""获取配置信息"""
return StandardResponse(
code=200,
message="success",
data=ConfigResponse(
label_studio_url=settings.label_studio_base_url,
)
)

View File

@@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db from app.db.session import get_db
from app.db.models import LabelingProject
from app.module.shared.schema import StandardResponse, PaginatedData from app.module.shared.schema import StandardResponse, PaginatedData
from app.module.dataset import DatasetManagementService from app.module.dataset import DatasetManagementService
from app.core.logging import get_logger from app.core.logging import get_logger
@@ -12,6 +13,7 @@ from app.core.config import settings
from ..client import LabelStudioClient from ..client import LabelStudioClient
from ..service.mapping import DatasetMappingService from ..service.mapping import DatasetMappingService
from ..service.sync import SyncService
from ..schema import ( from ..schema import (
DatasetMappingCreateRequest, DatasetMappingCreateRequest,
DatasetMappingCreateResponse, DatasetMappingCreateResponse,
@@ -25,7 +27,7 @@ router = APIRouter(
) )
logger = get_logger(__name__) logger = get_logger(__name__)
@router.post("/", response_model=StandardResponse[DatasetMappingCreateResponse], status_code=201) @router.post("", response_model=StandardResponse[DatasetMappingCreateResponse], status_code=201)
async def create_mapping( async def create_mapping(
request: DatasetMappingCreateRequest, request: DatasetMappingCreateRequest,
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
@@ -42,7 +44,8 @@ async def create_mapping(
dm_client = DatasetManagementService(db) dm_client = DatasetManagementService(db)
ls_client = LabelStudioClient(base_url=settings.label_studio_base_url, ls_client = LabelStudioClient(base_url=settings.label_studio_base_url,
token=settings.label_studio_user_token) token=settings.label_studio_user_token)
service = DatasetMappingService(db) mapping_service = DatasetMappingService(db)
sync_service = SyncService(dm_client, ls_client, mapping_service)
logger.info(f"Create dataset mapping request: {request.dataset_id}") logger.info(f"Create dataset mapping request: {request.dataset_id}")
@@ -54,24 +57,18 @@ async def create_mapping(
detail=f"Dataset not found in DM service: {request.dataset_id}" detail=f"Dataset not found in DM service: {request.dataset_id}"
) )
# 确定数据类型(基于数据集类型) project_name = request.name or \
data_type = "image" # 默认值 dataset_info.name or \
if dataset_info.type and dataset_info.type.code: "A new project from DataMate"
type_code = dataset_info.type.code.lower()
if "audio" in type_code:
data_type = "audio"
elif "video" in type_code:
data_type = "video"
elif "text" in type_code:
data_type = "text"
project_name = f"{dataset_info.name}"
project_description = request.description or \
dataset_info.description or \
f"Imported from DM dataset {dataset_info.name} ({dataset_info.id})"
# 在Label Studio中创建项目 # 在Label Studio中创建项目
project_data = await ls_client.create_project( project_data = await ls_client.create_project(
title=project_name, title=project_name,
description=dataset_info.description or f"Imported from DM dataset {dataset_info.id}", description=project_description,
data_type=data_type
) )
if not project_data: if not project_data:
@@ -97,13 +94,18 @@ async def create_mapping(
logger.warning(f"Failed to configure local storage for project {project_id}") logger.warning(f"Failed to configure local storage for project {project_id}")
else: else:
logger.info(f"Local storage configured for project {project_id}: {local_storage_path}") logger.info(f"Local storage configured for project {project_id}: {local_storage_path}")
labeling_project = LabelingProject(
dataset_id=request.dataset_id,
labeling_project_id=str(project_id),
name=project_name,
)
# 创建映射关系,包含项目名称(先持久化映射以获得 mapping.id)
mapping = await mapping_service.create_mapping(labeling_project)
# 创建映射关系,包含项目名称 # 进行一次同步,使用创建后的 mapping.id
mapping = await service.create_mapping( await sync_service.sync_dataset_files(mapping.id, 100)
request,
str(project_id),
project_name
)
response_data = DatasetMappingCreateResponse( response_data = DatasetMappingCreateResponse(
id=mapping.id, id=mapping.id,
@@ -123,7 +125,7 @@ async def create_mapping(
logger.error(f"Error while creating dataset mapping: {e}") logger.error(f"Error while creating dataset mapping: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/", response_model=StandardResponse[PaginatedData[DatasetMappingResponse]]) @router.get("", response_model=StandardResponse[PaginatedData[DatasetMappingResponse]])
async def list_mappings( async def list_mappings(
page: int = Query(1, ge=1, description="页码(从1开始)"), page: int = Query(1, ge=1, description="页码(从1开始)"),
page_size: int = Query(20, ge=1, le=100, description="每页记录数"), page_size: int = Query(20, ge=1, le=100, description="每页记录数"),
@@ -260,7 +262,7 @@ async def get_mappings_by_source(
logger.error(f"Error getting mappings: {e}") logger.error(f"Error getting mappings: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@router.delete("/", response_model=StandardResponse[DeleteDatasetResponse]) @router.delete("", response_model=StandardResponse[DeleteDatasetResponse])
async def delete_mapping( async def delete_mapping(
m: Optional[str] = Query(None, description="映射UUID"), m: Optional[str] = Query(None, description="映射UUID"),
proj: Optional[str] = Query(None, description="Label Studio项目ID"), proj: Optional[str] = Query(None, description="Label Studio项目ID"),
@@ -279,8 +281,11 @@ async def delete_mapping(
2. 软删除数据库中的映射记录 2. 软删除数据库中的映射记录
""" """
try: try:
# Log incoming request parameters for debugging
logger.debug(f"Delete mapping request received: m={m!r}, proj={proj!r}")
# 至少需要提供一个参数 # 至少需要提供一个参数
if not m and not proj: if not m and not proj:
logger.debug("Missing both 'm' and 'proj' in delete request")
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="Either 'm' (mapping UUID) or 'proj' (project ID) must be provided" detail="Either 'm' (mapping UUID) or 'proj' (project ID) must be provided"
@@ -300,6 +305,8 @@ async def delete_mapping(
mapping = await service.get_mapping_by_labeling_project_id(proj) mapping = await service.get_mapping_by_labeling_project_id(proj)
else: else:
mapping = None mapping = None
logger.debug(f"Mapping lookup result: {mapping}")
if not mapping: if not mapping:
raise HTTPException( raise HTTPException(
@@ -309,12 +316,12 @@ async def delete_mapping(
id = mapping.id id = mapping.id
labeling_project_id = mapping.labeling_project_id labeling_project_id = mapping.labeling_project_id
labeling_project_name = mapping.name
logger.debug(f"Found mapping: {id}, Label Studio project ID: {labeling_project_id}") logger.debug(f"Found mapping: {id}, Label Studio project ID: {labeling_project_id}")
# 1. 删除 Label Studio 项目 # 1. 删除 Label Studio 项目
try: try:
logger.debug(f"Deleting Label Studio project: {labeling_project_id}")
delete_success = await ls_client.delete_project(int(labeling_project_id)) delete_success = await ls_client.delete_project(int(labeling_project_id))
if delete_success: if delete_success:
logger.debug(f"Successfully deleted Label Studio project: {labeling_project_id}") logger.debug(f"Successfully deleted Label Studio project: {labeling_project_id}")
@@ -326,6 +333,7 @@ async def delete_mapping(
# 2. 软删除映射记录 # 2. 软删除映射记录
soft_delete_success = await service.soft_delete_mapping(id) soft_delete_success = await service.soft_delete_mapping(id)
logger.debug(f"Soft delete result for mapping {id}: {soft_delete_success}")
if not soft_delete_success: if not soft_delete_success:
raise HTTPException( raise HTTPException(

View File

@@ -1,5 +1,6 @@
from .config import ConfigResponse
from .mapping import ( from .mapping import (
_DatasetMappingBase,
DatasetMappingCreateRequest, DatasetMappingCreateRequest,
DatasetMappingCreateResponse, DatasetMappingCreateResponse,
DatasetMappingUpdateRequest, DatasetMappingUpdateRequest,
@@ -13,7 +14,7 @@ from .sync import (
) )
__all__ = [ __all__ = [
"_DatasetMappingBase", "ConfigResponse",
"DatasetMappingCreateRequest", "DatasetMappingCreateRequest",
"DatasetMappingCreateResponse", "DatasetMappingCreateResponse",
"DatasetMappingUpdateRequest", "DatasetMappingUpdateRequest",

View File

@@ -5,7 +5,4 @@ from app.module.shared.schema import StandardResponse
class ConfigResponse(BaseResponseModel): class ConfigResponse(BaseResponseModel):
"""配置信息响应模型""" """配置信息响应模型"""
app_name: str = Field(..., description="应用名称") label_studio_url: str = Field(..., description="Label Studio基础URL")
version: str = Field(..., description="应用版本")
label_studio_url: str = Field(..., description="Label Studio基础URL")
debug: bool = Field(..., description="调试模式状态")

View File

@@ -1,17 +1,27 @@
from pydantic import Field from pydantic import Field, BaseModel
from typing import Optional from typing import Optional
from datetime import datetime from datetime import datetime
from app.module.shared.schema import BaseResponseModel from app.module.shared.schema import BaseResponseModel
from app.module.shared.schema import StandardResponse from app.module.shared.schema import StandardResponse
class _DatasetMappingBase(BaseResponseModel):
"""数据集映射 基础模型"""
dataset_id: str = Field(..., description="源数据集ID")
class DatasetMappingCreateRequest(_DatasetMappingBase): class DatasetMappingCreateRequest(BaseModel):
"""数据集映射 创建 请求模型""" """数据集映射 创建 请求模型
pass
Accept both snake_case and camelCase field names from frontend JSON by
declaring explicit aliases. Frontend sends `datasetId`, `name`,
`description` (camelCase), so provide aliases so pydantic will map them
to the internal attributes used in the service code (dataset_id, name,
description).
"""
dataset_id: str = Field(..., alias="datasetId", description="源数据集ID")
name: Optional[str] = Field(None, alias="name", description="标注项目名称")
description: Optional[str] = Field(None, alias="description", description="标注项目描述")
class Config:
# allow population by field name when constructing model programmatically
allow_population_by_field_name = True
class DatasetMappingCreateResponse(BaseResponseModel): class DatasetMappingCreateResponse(BaseResponseModel):
"""数据集映射 创建 响应模型""" """数据集映射 创建 响应模型"""
@@ -23,7 +33,8 @@ class DatasetMappingUpdateRequest(BaseResponseModel):
"""数据集映射 更新 请求模型""" """数据集映射 更新 请求模型"""
dataset_id: Optional[str] = Field(None, description="源数据集ID") dataset_id: Optional[str] = Field(None, description="源数据集ID")
class DatasetMappingResponse(_DatasetMappingBase): class DatasetMappingResponse(BaseModel):
dataset_id: str = Field(..., description="源数据集ID")
"""数据集映射 查询 响应模型""" """数据集映射 查询 响应模型"""
id: str = Field(..., description="映射UUID") id: str = Field(..., description="映射UUID")
labeling_project_id: str = Field(..., description="标注项目ID") labeling_project_id: str = Field(..., description="标注项目ID")

View File

@@ -23,18 +23,16 @@ class DatasetMappingService:
async def create_mapping( async def create_mapping(
self, self,
mapping_data: DatasetMappingCreateRequest, labeling_project: LabelingProject
labeling_project_id: str,
labeling_project_name: str
) -> DatasetMappingResponse: ) -> DatasetMappingResponse:
"""创建数据集映射""" """创建数据集映射"""
logger.info(f"Create dataset mapping: {mapping_data.dataset_id} -> {labeling_project_id}") logger.info(f"Create dataset mapping: {labeling_project.dataset_id} -> {labeling_project.labeling_project_id}")
db_mapping = LabelingProject( db_mapping = LabelingProject(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
dataset_id=mapping_data.dataset_id, dataset_id=labeling_project.dataset_id,
labeling_project_id=labeling_project_id, labeling_project_id=labeling_project.labeling_project_id,
name=labeling_project_name name=labeling_project.name
) )
self.db.add(db_mapping) self.db.add(db_mapping)

View File

@@ -94,22 +94,22 @@ class SyncService:
async def sync_dataset_files( async def sync_dataset_files(
self, self,
id: str, mapping_id: str,
batch_size: int = 50 batch_size: int = 50
) -> SyncDatasetResponse: ) -> SyncDatasetResponse:
"""同步数据集文件到Label Studio""" """同步数据集文件到Label Studio"""
logger.info(f"Start syncing dataset by mapping: {id}") logger.info(f"Start syncing dataset by mapping: {mapping_id}")
# 获取映射关系 # 获取映射关系
mapping = await self.mapping_service.get_mapping_by_uuid(id) mapping = await self.mapping_service.get_mapping_by_uuid(mapping_id)
if not mapping: if not mapping:
logger.error(f"Dataset mapping not found: {id}") logger.error(f"Dataset mapping not found: {mapping_id}")
return SyncDatasetResponse( return SyncDatasetResponse(
id="", id="",
status="error", status="error",
synced_files=0, synced_files=0,
total_files=0, total_files=0,
message=f"Dataset mapping not found: {id}" message=f"Dataset mapping not found: {mapping_id}"
) )
try: try:

View File

@@ -3,7 +3,7 @@ from typing import Dict, Any
from app.core.config import settings from app.core.config import settings
from app.module.shared.schema import StandardResponse from app.module.shared.schema import StandardResponse
from ..schema import ConfigResponse, HealthResponse from ..schema import HealthResponse
router = APIRouter() router = APIRouter()
@@ -19,18 +19,4 @@ async def health_check():
service="Label Studio Adapter", service="Label Studio Adapter",
version=settings.app_version version=settings.app_version
) )
)
@router.get("/config", response_model=StandardResponse[ConfigResponse])
async def get_config():
"""获取配置信息"""
return StandardResponse(
code=200,
message="success",
data=ConfigResponse(
app_name=settings.app_name,
version=settings.app_version,
label_studio_url=settings.label_studio_base_url,
debug=settings.debug
)
) )

View File

@@ -1,4 +1,3 @@
from .config import ConfigResponse
from .health import HealthResponse from .health import HealthResponse
__all__ = ["ConfigResponse", "HealthResponse"] __all__ = ["HealthResponse"]