**使用中文与用户对话** # 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 编辑器!