**使用中文与用户对话**
# LabelStudio 编辑器集成规范
## 概述
本文档记录了在 React 应用中集成 LabelStudio 编辑器的完整流程、常见问题和解决方案。这些经验来自实际项目中遇到的样式问题和配置挑战。
## 核心原则
1. **动态导入编辑器**:使用 `import('@humansignal/editor')` 动态加载,不要静态导入
2. **CSS 模块前缀**:必须配置 webpack 添加 `lsf-` 前缀
3. **样式加载顺序**:只导入 UI 库样式,让编辑器样式自动加载
4. **参考成功案例**:遵循 playground 和 labelstudio 应用的实现方式
## 项目结构
```
web/
├── apps/
│ ├── lq_label/ # 你的应用
│ ├── playground/ # 参考:简单的编辑器集成
│ └── labelstudio/ # 参考:完整的 Label Studio 应用
├── libs/
│ ├── editor/ # LabelStudio 编辑器库
│ └── ui/ # UI 组件库
└── webpack.config.js # 根配置(包含 CSS 前缀处理)
```
## 集成步骤
### 1. Webpack 配置(最关键)
在应用的 `webpack.config.js` 中添加 CSS 模块配置:
```javascript
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 组件)
### 2. 样式导入(main.tsx)
**正确的方式**:
```typescript
// 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(
);
```
**错误的方式**(不要这样做):
```typescript
// ❌ 不要静态导入 editor 的全局样式
import '../../../libs/editor/src/assets/styles/global.scss';
```
### 3. 组件实现
参考 playground 的 `PreviewPanel` 组件实现:
```typescript
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(null);
const editorContainerRef = useRef(null);
const lsfInstanceRef = useRef(null);
const rafIdRef = useRef(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 (
{editorReady ? (
) : (
加载中...
)}
);
};
```
### 4. 组件样式(.module.scss)
```scss
.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%;
}
```
## 常见问题和解决方案
### 问题 1:样式完全不显示
**症状**:
- 标签按钮没有颜色
- 按钮没有边框和背景
- 整体看起来像是没有加载 CSS
**原因**:
- Webpack 配置缺少 CSS 模块的 `lsf-` 前缀处理
- HTML 元素有 `lsf-label` 类名,但 CSS 只有 `.label` 选择器
**解决方案**:
1. 在 webpack.config.js 中添加 CSS 模块配置(见上文)
2. 重启开发服务器(必须重启!)
3. 硬刷新浏览器(Ctrl+Shift+R)
**验证方法**:
```bash
# 1. 检查 HTML 元素
Positive
# 2. 检查 CSS 文件(Network 标签)
.lsf-label {
/* 样式定义 */
}
# 3. 两者应该匹配
```
### 问题 2:橙色到粉色的渐变遮罩
**症状**:
- 整个编辑器被一个橙色渐变覆盖
- 无法点击或交互
**原因**:
- `relations-overlay` 是 LabelStudio 的关系标注覆盖层(SVG)
- 默认有一个背景色
**解决方案**:
在组件样式中添加:
```scss
:global(.relations-overlay) {
background: transparent !important;
fill: none !important;
}
:global(.lsf-container) {
background: transparent !important;
}
```
### 问题 3:静态导入 editor 样式导致冲突
**症状**:
- 样式加载但不生效
- 或者样式冲突
**原因**:
- 静态导入 `editor/src/assets/styles/global.scss` 会导致样式加载顺序问题
- 与 Tailwind CSS 冲突
**解决方案**:
- 不要静态导入 editor 的全局样式
- 让样式随动态导入自动加载
- 参考 playground 和 labelstudio 应用的实现
### 问题 4:TypeScript 类型错误
**症状**:
```
Could not find a declaration file for module '@humansignal/editor'
```
**解决方案**:
在 webpack 配置中添加:
```javascript
withNx({
skipTypeChecking: true,
})
```
### 问题 5:音频解码器错误
**症状**:
```
Can't resolve '@humansignal/audio-file-decoder'
```
**解决方案**:
1. 创建空模块:
```javascript
// src/utils/empty-module.js
module.exports = {};
```
2. 在 webpack 配置中添加别名:
```javascript
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
'@humansignal/audio-file-decoder': require.resolve('./src/utils/empty-module.js'),
},
};
```
## 调试技巧
### 1. 使用浏览器开发者工具
**检查 HTML 元素**:
```
右键点击元素 → 检查
查看类名:应该是 lsf-label, lsf-button 等
```
**检查 CSS 文件**:
```
Network 标签 → 找到 libs_editor_src_index_js.css
搜索 .lsf-label
应该能找到样式定义
```
**检查样式应用**:
```
Elements 标签 → Styles 面板
查看 .lsf-label 的样式是否被应用
检查是否有被覆盖的样式
```
### 2. 控制台日志
在组件中添加详细日志:
```typescript
console.log('开始加载 LabelStudio 编辑器...');
console.log('LabelStudio 加载成功');
console.log('初始化 LabelStudio 实例...');
console.log('Storage 初始化完成');
console.log('Annotation 创建成功');
console.log('Annotation 更新:', result);
```
### 3. 创建测试页面
创建一个独立的测试页面来隔离问题:
```typescript
// src/views/editor-test/editor-test.tsx
// 使用简单的配置和数据
// 添加调试面板显示状态和错误
```
## 最佳实践
### 1. 参考成功案例
**Playground 应用**(推荐参考):
- 简单的编辑器集成
- 清晰的组件结构
- 位置:`web/apps/playground/src/components/PreviewPanel/`
**LabelStudio 应用**(完整实现):
- 完整的 Label Studio 应用
- 复杂的功能集成
- 位置:`web/apps/labelstudio/`
### 2. 样式隔离
使用 CSS 模块和 `:global()` 选择器:
```scss
.root {
// 组件自己的样式
// LabelStudio 样式覆盖
:global(.lsf-wrapper) {
// 全局样式
}
}
```
### 3. 清理逻辑
始终实现完整的清理逻辑:
```typescript
function cleanup() {
// 清除 window.LabelStudio
// 销毁编辑器实例
// 取消动画帧
// 移除事件监听器
}
useEffect(() => {
// 初始化逻辑
return () => {
cleanup();
};
}, [dependencies]);
```
### 4. 错误处理
提供友好的错误提示:
```typescript
const [error, setError] = useState(null);
try {
// 加载和初始化
} catch (err: any) {
console.error('Error loading LabelStudio:', err);
setError(err.message || '初始化编辑器失败');
}
// 在 UI 中显示错误
{error && (
{error}
)}
```
## 配置检查清单
在集成 LabelStudio 之前,确保:
- [ ] Webpack 配置添加了 CSS 前缀处理
- [ ] 定义了 `CSS_PREFIX` 环境变量
- [ ] 配置了 CSS 模块的 `localIdentName`
- [ ] main.tsx 只导入 UI 库样式
- [ ] 没有静态导入 editor 的全局样式
- [ ] 使用动态导入 `import('@humansignal/editor')`
- [ ] 实现了完整的清理逻辑
- [ ] 添加了 relations-overlay 透明背景修复
- [ ] 配置了 WebAssembly 支持
- [ ] 添加了 Node.js 模块 fallback
- [ ] 处理了音频解码器问题
## 故障排除流程
1. **检查 webpack 配置**
- 确认 CSS 前缀配置已添加
- 确认服务器已重启
2. **检查样式加载**
- 打开 Network 标签
- 确认 `libs_editor_src_index_js.css` 已加载
- 检查文件大小(应该是几 MB)
3. **检查类名匹配**
- HTML 元素的类名应该以 `lsf-` 开头
- CSS 文件中的选择器也应该以 `lsf-` 开头
4. **清除缓存**
- 硬刷新浏览器(Ctrl+Shift+R)
- 清除浏览器缓存
- 重启开发服务器
5. **查看控制台**
- 检查是否有 JavaScript 错误
- 检查是否有 CSS 加载错误
- 查看自定义日志输出
## 相关资源
- **LabelStudio 文档**:https://labelstud.io/guide/
- **CSS 模块文档**:https://github.com/css-modules/css-modules
- **Webpack CSS Loader**:https://webpack.js.org/loaders/css-loader/
- **项目参考**:
- `web/apps/playground/` - 简单集成示例
- `web/apps/labelstudio/` - 完整应用示例
- `web/webpack.config.js` - 根配置参考
## 总结
集成 LabelStudio 编辑器的关键是:
1. **正确的 Webpack 配置**:添加 CSS 模块的 `lsf-` 前缀处理
2. **正确的样式导入**:只导入 UI 库样式,让编辑器样式自动加载
3. **动态导入编辑器**:使用 `import('@humansignal/editor')`
4. **样式修复**:添加 relations-overlay 透明背景
5. **参考成功案例**:遵循 playground 和 labelstudio 的实现方式
遵循这些规范,可以避免大部分常见问题,快速集成 LabelStudio 编辑器!