diff --git a/frontend/src/components/business/LabelStudioEmbed.tsx b/frontend/src/components/business/LabelStudioEmbed.tsx new file mode 100644 index 0000000..8063e85 --- /dev/null +++ b/frontend/src/components/business/LabelStudioEmbed.tsx @@ -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(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 ( +
+ {!lsReady && ( +
+ +
+ )} +