You've already forked DataMate
LSF
This commit is contained in:
287
frontend/public/lsf/lsf.html
Normal file
287
frontend/public/lsf/lsf.html
Normal file
@@ -0,0 +1,287 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DataMate - Label Studio 编辑器</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
#label-studio {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--
|
||||
说明:
|
||||
- 本页面作为 iframe 运行,用于隔离 Label Studio Frontend(避免与 DataMate React/Antd 依赖冲突)。
|
||||
- 当前使用 CDN 加载 LSF 产物;如需离线部署,可改为本地静态资源。
|
||||
- 与父页面通过 postMessage 通信,约定消息类型:
|
||||
- Parent -> Iframe: LS_INIT / LS_EXPORT / LS_RESET / LS_PING
|
||||
- Iframe -> Parent: LS_IFRAME_READY / LS_READY / LS_EXPORT_RESULT / LS_RESET_DONE / LS_PONG / LS_ERROR / LS_SUBMIT
|
||||
-->
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/label-studio-frontend@1.7.1/dist/lsf/css/main.css"
|
||||
/>
|
||||
<script src="https://unpkg.com/label-studio-frontend@1.7.1/dist/lsf/js/main.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="label-studio"></div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const ORIGIN = window.location.origin;
|
||||
|
||||
let lsInstance = null;
|
||||
let currentTask = null;
|
||||
|
||||
function postToParent(type, payload) {
|
||||
window.parent.postMessage({ type, payload }, ORIGIN);
|
||||
}
|
||||
|
||||
function destroyLabelStudio() {
|
||||
try {
|
||||
if (lsInstance && typeof lsInstance.destroy === "function") {
|
||||
lsInstance.destroy();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
lsInstance = null;
|
||||
currentTask = null;
|
||||
|
||||
const root = document.getElementById("label-studio");
|
||||
if (root) root.innerHTML = "";
|
||||
}
|
||||
|
||||
function pickAnnotationStore(ls) {
|
||||
return ls?.annotationStore || ls?.store?.annotationStore || null;
|
||||
}
|
||||
|
||||
function normalizeUser(rawUser) {
|
||||
const user = rawUser && typeof rawUser === "object" ? rawUser : { id: "anonymous" };
|
||||
const userId = user.id || user.userId || user.username || user.email || "anonymous";
|
||||
|
||||
let pk = user.pk;
|
||||
if (!pk) {
|
||||
let h = 0;
|
||||
for (let i = 0; i < String(userId).length; i++) {
|
||||
h = (h * 31 + String(userId).charCodeAt(i)) | 0;
|
||||
}
|
||||
pk = Math.abs(h) || 1;
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
id: userId,
|
||||
pk,
|
||||
firstName: user.firstName || user.name || String(userId),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTask(task, extra) {
|
||||
const t = task && typeof task === "object" ? { ...task } : null;
|
||||
if (!t) return null;
|
||||
|
||||
const annotations = Array.isArray(extra?.annotations)
|
||||
? extra.annotations
|
||||
: Array.isArray(t.annotations)
|
||||
? t.annotations
|
||||
: [];
|
||||
|
||||
const predictions = Array.isArray(extra?.predictions)
|
||||
? extra.predictions
|
||||
: Array.isArray(t.predictions)
|
||||
? t.predictions
|
||||
: [];
|
||||
|
||||
return {
|
||||
...t,
|
||||
annotations,
|
||||
predictions,
|
||||
};
|
||||
}
|
||||
|
||||
function ensureSelectedAnnotation(store, prefer) {
|
||||
if (!store) return;
|
||||
|
||||
const annotations = Array.isArray(store.annotations) ? store.annotations : [];
|
||||
|
||||
if (prefer) {
|
||||
const byId = annotations.find((a) => String(a.id) === String(prefer.id));
|
||||
if (byId && typeof store.selectAnnotation === "function") {
|
||||
store.selectAnnotation(byId.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = Number.isFinite(prefer.index) ? Number(prefer.index) : -1;
|
||||
if (idx >= 0 && idx < annotations.length && typeof store.selectAnnotation === "function") {
|
||||
store.selectAnnotation(annotations[idx].id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (annotations.length > 0 && typeof store.selectAnnotation === "function") {
|
||||
store.selectAnnotation(annotations[0].id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof store.addAnnotation === "function" && typeof store.selectAnnotation === "function") {
|
||||
const ann = store.addAnnotation({ userGenerate: true });
|
||||
if (ann && ann.id) store.selectAnnotation(ann.id);
|
||||
}
|
||||
}
|
||||
|
||||
function exportSelectedAnnotation() {
|
||||
if (!lsInstance) {
|
||||
throw new Error("LabelStudio 未初始化");
|
||||
}
|
||||
|
||||
const store = pickAnnotationStore(lsInstance);
|
||||
if (!store) {
|
||||
throw new Error("无法访问 annotationStore");
|
||||
}
|
||||
|
||||
const selected =
|
||||
store.selected ||
|
||||
store.selectedAnnotation ||
|
||||
(Array.isArray(store.annotations) && store.annotations.length ? store.annotations[0] : null);
|
||||
|
||||
let serialized = null;
|
||||
if (selected && typeof selected.serializeAnnotation === "function") {
|
||||
serialized = selected.serializeAnnotation();
|
||||
} else if (selected && typeof selected.serialize === "function") {
|
||||
serialized = selected.serialize();
|
||||
}
|
||||
|
||||
const annotationPayload = Array.isArray(serialized)
|
||||
? { id: selected?.id || "draft", result: serialized }
|
||||
: serialized && typeof serialized === "object"
|
||||
? { id: selected?.id || serialized.id || "draft", ...serialized }
|
||||
: { id: selected?.id || "draft", result: (selected && selected.result) || [] };
|
||||
|
||||
// 最小化对齐 Label Studio Server 的字段(DataMate 侧会原样存储)
|
||||
if (!annotationPayload.task) annotationPayload.task = currentTask?.id || null;
|
||||
if (!annotationPayload.created_at) annotationPayload.created_at = new Date().toISOString();
|
||||
annotationPayload.updated_at = new Date().toISOString();
|
||||
|
||||
return {
|
||||
taskId: currentTask?.id || null,
|
||||
annotation: annotationPayload,
|
||||
};
|
||||
}
|
||||
|
||||
function initLabelStudio(payload) {
|
||||
if (!window.LabelStudio) {
|
||||
throw new Error("LabelStudio 未加载(请检查静态资源/网络)");
|
||||
}
|
||||
if (!payload || !payload.labelConfig || !payload.task) {
|
||||
throw new Error("初始化参数缺失:labelConfig/task");
|
||||
}
|
||||
|
||||
destroyLabelStudio();
|
||||
|
||||
const interfaces =
|
||||
payload.interfaces ||
|
||||
[
|
||||
"panel",
|
||||
"update",
|
||||
"controls",
|
||||
"side-column",
|
||||
"annotations",
|
||||
"infobar",
|
||||
"instruction",
|
||||
];
|
||||
|
||||
const task = normalizeTask(payload.task, payload);
|
||||
if (!task) {
|
||||
throw new Error("task 参数非法");
|
||||
}
|
||||
|
||||
currentTask = task;
|
||||
const user = normalizeUser(payload.user);
|
||||
|
||||
lsInstance = new window.LabelStudio("label-studio", {
|
||||
config: payload.labelConfig,
|
||||
interfaces,
|
||||
user,
|
||||
task,
|
||||
onLabelStudioLoad: function (LS) {
|
||||
try {
|
||||
const store = pickAnnotationStore(LS);
|
||||
ensureSelectedAnnotation(store, {
|
||||
id: payload.selectedAnnotationId,
|
||||
index: payload.selectedAnnotationIndex,
|
||||
});
|
||||
|
||||
// 允许在没有任何 annotation 的情况下,自动创建一个可编辑的 annotation
|
||||
if (payload.allowCreateEmptyAnnotation !== false) {
|
||||
try {
|
||||
const store2 = pickAnnotationStore(LS);
|
||||
const selected = store2?.selected || store2?.selectedAnnotation || null;
|
||||
if (!selected && typeof store2?.addAnnotation === "function" && typeof store2?.selectAnnotation === "function") {
|
||||
const ann = store2.addAnnotation({ userGenerate: true });
|
||||
if (ann?.id) store2.selectAnnotation(ann.id);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
postToParent("LS_READY", { taskId: task?.id || null });
|
||||
},
|
||||
// 让内嵌编辑器的“提交/保存”按钮也能触发父页面保存
|
||||
onSubmitAnnotation: function () {
|
||||
try {
|
||||
const raw = exportSelectedAnnotation();
|
||||
postToParent("LS_SUBMIT", raw);
|
||||
} catch (e) {
|
||||
postToParent("LS_ERROR", { message: e?.message || String(e) });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.origin !== ORIGIN) return;
|
||||
|
||||
const msg = event.data || {};
|
||||
if (!msg.type) return;
|
||||
|
||||
try {
|
||||
if (msg.type === "LS_INIT") {
|
||||
initLabelStudio(msg.payload || {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "LS_EXPORT") {
|
||||
const raw = exportSelectedAnnotation();
|
||||
postToParent("LS_EXPORT_RESULT", raw);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "LS_RESET") {
|
||||
destroyLabelStudio();
|
||||
postToParent("LS_RESET_DONE", {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "LS_PING") {
|
||||
postToParent("LS_PONG", {});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
postToParent("LS_ERROR", { message: e?.message || String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
postToParent("LS_IFRAME_READY", {});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user