From d5b75fee0d32921443f48c8d787cb03facb7a1d5 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 7 Jan 2026 00:00:16 +0800 Subject: [PATCH] LSF --- frontend/public/lsf/lsf.html | 287 +++++++++++++++ .../Annotate/LabelStudioTextEditor.tsx | 346 ++++++++++++++++++ .../DataAnnotation/Home/DataAnnotation.tsx | 114 ++---- .../pages/DataAnnotation/annotation.api.ts | 30 +- frontend/src/routes/routes.ts | 5 + runtime/datamate-python/app/core/config.py | 6 + .../datamate-python/app/db/models/__init__.py | 4 +- .../app/db/models/annotation_management.py | 40 +- .../module/annotation/interface/__init__.py | 14 +- .../app/module/annotation/interface/editor.py | 90 +++++ .../app/module/annotation/schema/__init__.py | 17 +- .../app/module/annotation/schema/editor.py | 83 +++++ .../app/module/annotation/service/editor.py | 295 +++++++++++++++ scripts/db/data-annotation-init.sql | 60 +-- 14 files changed, 1267 insertions(+), 124 deletions(-) create mode 100644 frontend/public/lsf/lsf.html create mode 100644 frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx create mode 100644 runtime/datamate-python/app/module/annotation/interface/editor.py create mode 100644 runtime/datamate-python/app/module/annotation/schema/editor.py create mode 100644 runtime/datamate-python/app/module/annotation/service/editor.py diff --git a/frontend/public/lsf/lsf.html b/frontend/public/lsf/lsf.html new file mode 100644 index 0000000..ac70325 --- /dev/null +++ b/frontend/public/lsf/lsf.html @@ -0,0 +1,287 @@ + + + + + + DataMate - Label Studio 编辑器 + + + + + + + + + +
+ + + + + diff --git a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx new file mode 100644 index 0000000..345bf24 --- /dev/null +++ b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx @@ -0,0 +1,346 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { App, Button, Card, List, Spin, Typography } from "antd"; +import { LeftOutlined, ReloadOutlined, SaveOutlined } from "@ant-design/icons"; +import { useNavigate, useParams } from "react-router"; + +import { + getEditorProjectInfoUsingGet, + getEditorTaskUsingGet, + listEditorTasksUsingGet, + upsertEditorAnnotationUsingPut, +} from "../annotation.api"; + +type EditorProjectInfo = { + projectId: string; + datasetId: string; + templateId?: string | null; + labelConfig?: string | null; + supported: boolean; + unsupportedReason?: string | null; +}; + +type EditorTaskListItem = { + fileId: string; + fileName: string; + fileType?: string | null; + hasAnnotation: boolean; + annotationUpdatedAt?: string | null; +}; + +type LsfMessage = { + type?: string; + payload?: any; +}; + +const LSF_IFRAME_SRC = "/lsf/lsf.html"; + +export default function LabelStudioTextEditor() { + const { projectId = "" } = useParams(); + const navigate = useNavigate(); + const { message } = App.useApp(); + + const origin = useMemo(() => window.location.origin, []); + const iframeRef = useRef(null); + const initSeqRef = useRef(0); + + const [loadingProject, setLoadingProject] = useState(true); + const [loadingTasks, setLoadingTasks] = useState(false); + const [loadingTaskDetail, setLoadingTaskDetail] = useState(false); + const [saving, setSaving] = useState(false); + + const [iframeReady, setIframeReady] = useState(false); + const [project, setProject] = useState(null); + const [tasks, setTasks] = useState([]); + const [selectedFileId, setSelectedFileId] = useState(""); + + const postToIframe = (type: string, payload?: any) => { + const win = iframeRef.current?.contentWindow; + if (!win) return; + win.postMessage({ type, payload }, origin); + }; + + const loadProject = async () => { + setLoadingProject(true); + try { + const resp = (await getEditorProjectInfoUsingGet(projectId)) as any; + const data = resp?.data as EditorProjectInfo | undefined; + if (!data?.projectId) { + message.error("获取标注项目信息失败"); + setProject(null); + return; + } + setProject(data); + } catch (e) { + console.error(e); + message.error("获取标注项目信息失败"); + setProject(null); + } finally { + setLoadingProject(false); + } + }; + + const loadTasks = async (silent = false) => { + if (!projectId) return; + if (!silent) setLoadingTasks(true); + try { + const resp = (await listEditorTasksUsingGet(projectId, { page: 0, size: 200 })) as any; + const content = (resp?.data?.content || []) as EditorTaskListItem[]; + const items = Array.isArray(content) ? content : []; + setTasks(items); + if (!selectedFileId && items.length > 0) { + setSelectedFileId(items[0].fileId); + } + } catch (e) { + console.error(e); + if (!silent) message.error("获取文件列表失败"); + setTasks([]); + } finally { + if (!silent) setLoadingTasks(false); + } + }; + + const initEditorForFile = async (fileId: string) => { + if (!project?.supported) return; + if (!project?.labelConfig) { + message.error("该项目未绑定标注模板,无法加载编辑器"); + return; + } + if (!iframeReady) return; + + const seq = ++initSeqRef.current; + setLoadingTaskDetail(true); + + try { + const resp = (await getEditorTaskUsingGet(projectId, fileId)) as any; + const task = resp?.data?.task; + if (!task) { + message.error("获取任务详情失败"); + return; + } + if (seq !== initSeqRef.current) return; + + postToIframe("LS_INIT", { + labelConfig: project.labelConfig, + task, + user: { id: "datamate" }, + interfaces: [ + "panel", + "update", + "submit", + "controls", + "side-column", + "annotations:menu", + "annotations:add-new", + "annotations:delete", + ], + selectedAnnotationIndex: 0, + allowCreateEmptyAnnotation: true, + }); + } catch (e) { + console.error(e); + message.error("加载编辑器失败"); + } finally { + if (seq === initSeqRef.current) setLoadingTaskDetail(false); + } + }; + + const saveFromExport = async (payload: any) => { + const taskId = payload?.taskId; + const annotation = payload?.annotation; + if (!taskId || !annotation) { + message.error("导出标注失败:缺少 taskId/annotation"); + return; + } + + setSaving(true); + try { + await upsertEditorAnnotationUsingPut(projectId, String(taskId), { annotation }); + message.success("标注已保存"); + await loadTasks(true); + } catch (e) { + console.error(e); + message.error("保存失败"); + } finally { + setSaving(false); + } + }; + + const requestExport = () => { + if (!selectedFileId) { + message.warning("请先选择文件"); + return; + } + postToIframe("LS_EXPORT", {}); + }; + + useEffect(() => { + setIframeReady(false); + setProject(null); + setTasks([]); + setSelectedFileId(""); + initSeqRef.current = 0; + + if (projectId) loadProject(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [projectId]); + + useEffect(() => { + if (!project?.supported) return; + loadTasks(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [project?.projectId, project?.supported]); + + useEffect(() => { + if (!selectedFileId) return; + initEditorForFile(selectedFileId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedFileId, iframeReady]); + + useEffect(() => { + const handler = (event: MessageEvent) => { + if (event.origin !== origin) return; + const msg = event.data || {}; + if (!msg?.type) return; + + if (msg.type === "LS_IFRAME_READY") { + setIframeReady(true); + return; + } + + if (msg.type === "LS_EXPORT_RESULT") { + saveFromExport(msg.payload); + return; + } + + // 兼容 iframe 内部在 submit 时直接上报(若启用) + if (msg.type === "LS_SUBMIT") { + saveFromExport(msg.payload); + return; + } + + if (msg.type === "LS_ERROR") { + message.error(msg.payload?.message || "编辑器发生错误"); + } + }; + + window.addEventListener("message", handler); + return () => window.removeEventListener("message", handler); + }, [message, origin]); + + if (loadingProject) { + return ( +
+ +
+ ); + } + + if (!project) { + return ( +
+ + 未找到标注项目 +
+ +
+
+
+ ); + } + + if (!project.supported) { + return ( +
+ + 暂不支持该数据类型 + + {project.unsupportedReason || "当前仅支持文本(TEXT)项目的内嵌编辑器。"} + +
+ +
+
+
+ ); + } + + return ( +
+
+
+ + + 标注(内嵌编辑器) + +
+
+ + +
+
+ +
+ + ( + setSelectedFileId(item.fileId)} + > +
+
+ {item.fileName} + + {item.hasAnnotation ? "已标注" : "未标注"} + +
+ {item.annotationUpdatedAt && ( + + 更新: {item.annotationUpdatedAt} + + )} +
+
+ )} + /> +
+ + +
+ {loadingTaskDetail && ( +
+ +
+ )} +