You've already forked DataMate
feat(annotation): 添加标注任务自定义配置功能
- 新增 LabelStudioEmbed 组件用于嵌入式标注界面预览 - 在创建标注任务对话框中添加 XML 配置编辑器 - 支持从现有模板加载配置并进行自定义修改 - 实现标注界面实时预览功能 - 后端支持直接传递 label_config 覆盖模板配置 - 更新 CreateAnnotationTaskRequest 模型添加 labelConfig 字段
This commit is contained in:
120
frontend/src/components/business/LabelStudioEmbed.tsx
Normal file
120
frontend/src/components/business/LabelStudioEmbed.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useEffect, useRef, useState, useMemo } from "react";
|
||||
import { Spin, message } from "antd";
|
||||
|
||||
const LSF_IFRAME_SRC = "/lsf/lsf.html";
|
||||
|
||||
export interface LabelStudioEmbedProps {
|
||||
config: string;
|
||||
task?: any;
|
||||
user?: any;
|
||||
interfaces?: string[];
|
||||
height?: string | number;
|
||||
className?: string;
|
||||
onSubmit?: (result: any) => void;
|
||||
onUpdate?: (result: any) => void;
|
||||
onError?: (error: any) => void;
|
||||
}
|
||||
|
||||
export default function LabelStudioEmbed({
|
||||
config,
|
||||
task = { id: 1, data: { text: "Preview Text" } },
|
||||
user = { id: "preview-user" },
|
||||
interfaces = [
|
||||
"panel",
|
||||
"update",
|
||||
"controls",
|
||||
"side-column",
|
||||
"annotations:tabs",
|
||||
"annotations:menu",
|
||||
"annotations:current",
|
||||
"annotations:history",
|
||||
],
|
||||
height = "100%",
|
||||
className = "",
|
||||
onSubmit,
|
||||
onUpdate,
|
||||
onError,
|
||||
}: LabelStudioEmbedProps) {
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const [iframeReady, setIframeReady] = useState(false);
|
||||
const [lsReady, setLsReady] = useState(false);
|
||||
const origin = useMemo(() => window.location.origin, []);
|
||||
|
||||
const postToIframe = (type: string, payload?: any) => {
|
||||
const win = iframeRef.current?.contentWindow;
|
||||
if (!win) return;
|
||||
win.postMessage({ type, payload }, origin);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIframeReady(false);
|
||||
setLsReady(false);
|
||||
}, [config]); // Reset when config changes
|
||||
|
||||
// Initialize LS when iframe is ready and config is available
|
||||
useEffect(() => {
|
||||
if (iframeReady && config) {
|
||||
postToIframe("LS_INIT", {
|
||||
labelConfig: config,
|
||||
task,
|
||||
user,
|
||||
interfaces,
|
||||
selectedAnnotationIndex: 0,
|
||||
allowCreateEmptyAnnotation: true,
|
||||
});
|
||||
}
|
||||
}, [iframeReady, config, task, user, interfaces]);
|
||||
|
||||
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_READY") {
|
||||
setLsReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "LS_EXPORT_RESULT" || msg.type === "LS_SUBMIT") {
|
||||
if (onSubmit) onSubmit(msg.payload);
|
||||
else if (onUpdate) onUpdate(msg.payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "LS_UPDATE_ANNOTATION") {
|
||||
if (onUpdate) onUpdate(msg.payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "LS_ERROR") {
|
||||
if (onError) onError(msg.payload);
|
||||
else message.error(msg.payload?.message || "编辑器发生错误");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handler);
|
||||
return () => window.removeEventListener("message", handler);
|
||||
}, [origin, onSubmit, onUpdate, onError]);
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`} style={{ height }}>
|
||||
{!lsReady && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/70">
|
||||
<Spin tip={!iframeReady ? "加载资源..." : "初始化编辑器..."} />
|
||||
</div>
|
||||
)}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="Label Studio Frontend"
|
||||
src={LSF_IFRAME_SRC}
|
||||
className="w-full h-full border-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user