使用中文与用户对话
本文档记录了在 React 应用中集成 LabelStudio 编辑器的完整流程、常见问题和解决方案。这些经验来自实际项目中遇到的样式问题和配置挑战。
import('@humansignal/editor') 动态加载,不要静态导入lsf- 前缀web/
├── apps/
│ ├── lq_label/ # 你的应用
│ ├── playground/ # 参考:简单的编辑器集成
│ └── labelstudio/ # 参考:完整的 Label Studio 应用
├── libs/
│ ├── editor/ # LabelStudio 编辑器库
│ └── ui/ # UI 组件库
└── webpack.config.js # 根配置(包含 CSS 前缀处理)
在应用的 webpack.config.js 中添加 CSS 模块配置:
const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const webpack = require('webpack');
const css_prefix = 'lsf-';
module.exports = composePlugins(
withNx({ skipTypeChecking: true }),
withReact(),
(config) => {
// 1. 定义 CSS_PREFIX 环境变量
config.plugins = [
...config.plugins,
new webpack.DefinePlugin({
'process.env.CSS_PREFIX': JSON.stringify(css_prefix),
}),
];
// 2. 配置 CSS 模块添加 lsf- 前缀
config.module.rules.forEach((rule) => {
const testString = rule.test?.toString() || '';
if (rule.test?.toString().match(/scss|sass/) && !testString.includes('.module')) {
const r = rule.oneOf?.filter((r) => {
if (!r.use) return false;
const testString = r.test?.toString() || '';
if (testString.match(/module|raw|antd/)) return false;
return testString.match(/scss|sass/) &&
r.use.some((u) => u.loader && u.loader.includes('css-loader'));
});
r?.forEach((_r) => {
const cssLoader = _r.use.find((use) =>
use.loader && use.loader.includes('css-loader')
);
if (!cssLoader) return;
const isSASS = _r.use.some((use) =>
use.loader && use.loader.match(/sass|scss/)
);
if (isSASS) _r.exclude = /node_modules/;
if (cssLoader.options) {
cssLoader.options.modules = {
localIdentName: `${css_prefix}[local]`,
getLocalIdent(_ctx, _ident, className) {
if (className.includes('ant')) return className;
},
};
}
});
}
});
// 3. 其他必要配置
config.experiments = {
...config.experiments,
asyncWebAssembly: true,
syncWebAssembly: true,
topLevelAwait: true,
};
config.resolve = {
...config.resolve,
fallback: {
path: false,
fs: false,
crypto: false,
stream: false,
buffer: false,
util: false,
process: false,
},
};
return config;
}
);
关键点:
css_prefix = 'lsf-':所有 LabelStudio 的 CSS 类名前缀localIdentName: 'lsf-[local]':将 .label 转换为 .lsf-label.module.scss 文件(它们有自己的命名规则)ant 开头的类名(Ant Design 组件)正确的方式:
// main.tsx
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './app/app';
// 只导入 UI 库样式
// LabelStudio 编辑器样式会在动态导入时自动加载
import '../../../libs/ui/src/styles.scss';
import '../../../libs/ui/src/tailwind.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);
错误的方式(不要这样做):
// ❌ 不要静态导入 editor 的全局样式
import '../../../libs/editor/src/assets/styles/global.scss';
参考 playground 的 PreviewPanel 组件实现:
import React, { useEffect, useState, useRef } from 'react';
import { onSnapshot } from 'mobx-state-tree';
// 清除 localStorage
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('labelStudio:settings');
}
export const EditorComponent: React.FC = () => {
const [editorReady, setEditorReady] = useState(false);
const [error, setError] = useState<string | null>(null);
const editorContainerRef = useRef<HTMLDivElement>(null);
const lsfInstanceRef = useRef<any>(null);
const rafIdRef = useRef<number | null>(null);
useEffect(() => {
let LabelStudio: any;
let dependencies: any;
let snapshotDisposer: any;
function cleanup() {
if (typeof window !== 'undefined' && (window as any).LabelStudio) {
delete (window as any).LabelStudio;
}
setEditorReady(false);
if (lsfInstanceRef.current) {
try {
lsfInstanceRef.current.destroy();
} catch {
// Ignore cleanup errors in HMR scenarios
}
lsfInstanceRef.current = null;
}
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
}
if (snapshotDisposer) {
snapshotDisposer();
snapshotDisposer = null;
}
}
async function loadLSF() {
try {
// 动态导入 LabelStudio
dependencies = await import('@humansignal/editor');
LabelStudio = dependencies.LabelStudio;
if (!LabelStudio) {
setError('编辑器加载失败');
return;
}
cleanup();
setEditorReady(true);
// 初始化 LabelStudio 实例
setTimeout(() => {
if (!editorContainerRef.current) return;
lsfInstanceRef.current = new LabelStudio(editorContainerRef.current, {
config: yourConfig,
task: yourTask,
interfaces: [
'panel',
'update',
'submit',
'controls',
'side-column',
'annotations:menu',
'annotations:add-new',
'annotations:delete',
'predictions:menu',
],
instanceOptions: {
reactVersion: 'v18',
},
settings: {
forceBottomPanel: true,
collapsibleBottomPanel: true,
defaultCollapsedBottomPanel: false,
fullscreen: false,
},
onStorageInitialized: (LS: any) => {
const initAnnotation = () => {
const as = LS.annotationStore;
const annotation = as.createAnnotation();
as.selectAnnotation(annotation.id);
if (annotation) {
snapshotDisposer = onSnapshot(annotation, () => {
// 处理标注更新
const result = annotation.serializeAnnotation();
console.log('Annotation updated:', result);
});
}
};
setTimeout(initAnnotation);
},
});
});
} catch (err: any) {
console.error('Error loading LabelStudio:', err);
setError(err.message || '初始化编辑器失败');
}
}
rafIdRef.current = requestAnimationFrame(() => {
loadLSF();
});
return () => {
cleanup();
};
}, [yourConfig, yourTask]);
return (
<div className="editor-container">
{editorReady ? (
<div
ref={editorContainerRef}
className="w-full h-full flex flex-col"
/>
) : (
<div>加载中...</div>
)}
</div>
);
};
.root {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
// LabelStudio 样式覆盖
:global(.lsf-wrapper) {
flex: 1;
display: flex;
flex-direction: column;
}
:global(.lsf-editor) {
min-width: 320px;
width: 100%;
}
// 修复 relations-overlay 橙色遮罩问题
:global(.relations-overlay) {
background: transparent !important;
fill: none !important;
}
:global(.lsf-container) {
background: transparent !important;
}
}
.editorContainer {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
width: 100%;
}
症状:
原因:
lsf- 前缀处理lsf-label 类名,但 CSS 只有 .label 选择器解决方案:
验证方法:
# 1. 检查 HTML 元素
<div class="lsf-label lsf-label_selected">Positive</div>
# 2. 检查 CSS 文件(Network 标签)
.lsf-label {
/* 样式定义 */
}
# 3. 两者应该匹配
症状:
原因:
relations-overlay 是 LabelStudio 的关系标注覆盖层(SVG)解决方案: 在组件样式中添加:
:global(.relations-overlay) {
background: transparent !important;
fill: none !important;
}
:global(.lsf-container) {
background: transparent !important;
}
症状:
原因:
editor/src/assets/styles/global.scss 会导致样式加载顺序问题解决方案:
症状:
Could not find a declaration file for module '@humansignal/editor'
解决方案: 在 webpack 配置中添加:
withNx({
skipTypeChecking: true,
})
症状:
Can't resolve '@humansignal/audio-file-decoder'
解决方案:
创建空模块:
// src/utils/empty-module.js
module.exports = {};
在 webpack 配置中添加别名:
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'@humansignal/audio-file-decoder': require.resolve('./src/utils/empty-module.js'),
},
};
检查 HTML 元素:
右键点击元素 → 检查
查看类名:应该是 lsf-label, lsf-button 等
检查 CSS 文件:
Network 标签 → 找到 libs_editor_src_index_js.css
搜索 .lsf-label
应该能找到样式定义
检查样式应用:
Elements 标签 → Styles 面板
查看 .lsf-label 的样式是否被应用
检查是否有被覆盖的样式
在组件中添加详细日志:
console.log('开始加载 LabelStudio 编辑器...');
console.log('LabelStudio 加载成功');
console.log('初始化 LabelStudio 实例...');
console.log('Storage 初始化完成');
console.log('Annotation 创建成功');
console.log('Annotation 更新:', result);
创建一个独立的测试页面来隔离问题:
// src/views/editor-test/editor-test.tsx
// 使用简单的配置和数据
// 添加调试面板显示状态和错误
Playground 应用(推荐参考):
web/apps/playground/src/components/PreviewPanel/LabelStudio 应用(完整实现):
web/apps/labelstudio/使用 CSS 模块和 :global() 选择器:
.root {
// 组件自己的样式
// LabelStudio 样式覆盖
:global(.lsf-wrapper) {
// 全局样式
}
}
始终实现完整的清理逻辑:
function cleanup() {
// 清除 window.LabelStudio
// 销毁编辑器实例
// 取消动画帧
// 移除事件监听器
}
useEffect(() => {
// 初始化逻辑
return () => {
cleanup();
};
}, [dependencies]);
提供友好的错误提示:
const [error, setError] = useState<string | null>(null);
try {
// 加载和初始化
} catch (err: any) {
console.error('Error loading LabelStudio:', err);
setError(err.message || '初始化编辑器失败');
}
// 在 UI 中显示错误
{error && (
<div className="error-message">
{error}
</div>
)}
在集成 LabelStudio 之前,确保:
CSS_PREFIX 环境变量localIdentNameimport('@humansignal/editor')检查 webpack 配置
检查样式加载
libs_editor_src_index_js.css 已加载检查类名匹配
lsf- 开头lsf- 开头清除缓存
查看控制台
web/apps/playground/ - 简单集成示例web/apps/labelstudio/ - 完整应用示例web/webpack.config.js - 根配置参考集成 LabelStudio 编辑器的关键是:
lsf- 前缀处理import('@humansignal/editor')遵循这些规范,可以避免大部分常见问题,快速集成 LabelStudio 编辑器!