import { useEffect, useMemo, useRef, useState } from "react"; import { App, Button, Card, List, Spin, Typography } from "antd"; import { LeftOutlined, ReloadOutlined, SaveOutlined, MenuFoldOutlined, MenuUnfoldOutlined } 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 expectedTaskIdRef = useRef(null); 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 [lsReady, setLsReady] = useState(false); const [project, setProject] = useState(null); const [tasks, setTasks] = useState([]); const [selectedFileId, setSelectedFileId] = useState(""); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); 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); setLsReady(false); expectedTaskIdRef.current = null; try { const resp = (await getEditorTaskUsingGet(projectId, fileId)) as any; const task = resp?.data?.task; if (!task) { message.error("获取任务详情失败"); return; } if (seq !== initSeqRef.current) return; expectedTaskIdRef.current = Number(task?.id) || null; postToIframe("LS_INIT", { labelConfig: project.labelConfig, task, user: { id: "datamate" }, // 完整的 Label Studio 原生界面配置 interfaces: [ // 核心面板 "panel", // 导航面板(undo/redo/reset) "update", // 更新按钮 "submit", // 提交按钮 "controls", // 控制面板 // 侧边栏(包含 Outliner 和 Details) "side-column", // 标注管理 "annotations:tabs", "annotations:menu", "annotations:current", "annotations:add-new", "annotations:delete", "annotations:view-all", // 预测 "predictions:tabs", "predictions:menu", // 其他 "auto-annotation", "edit-history", ], selectedAnnotationIndex: 0, allowCreateEmptyAnnotation: true, }); } catch (e) { console.error(e); message.error("加载编辑器失败"); } finally { if (seq === initSeqRef.current) setLoadingTaskDetail(false); } }; const saveFromExport = async (payload: any) => { const fileId = payload?.fileId; const annotation = payload?.annotation; if (!fileId || !annotation) { message.error("导出标注失败:缺少 fileId/annotation"); return; } setSaving(true); try { await upsertEditorAnnotationUsingPut(projectId, String(fileId), { 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; setLsReady(false); expectedTaskIdRef.current = null; 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_READY") { const readyTaskId = msg.payload?.taskId; if (expectedTaskIdRef.current && readyTaskId) { if (Number(readyTaskId) !== expectedTaskIdRef.current) return; } setLsReady(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 || "编辑器发生错误"); setLsReady(false); } }; 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} )}
)} />
{/* 右侧编辑器 - Label Studio iframe */}
{(!iframeReady || loadingTaskDetail || (selectedFileId && !lsReady)) && (
)}