|
|
@@ -17,6 +17,7 @@ import {
|
|
|
import { getTask, getProject, createAnnotation, updateTask } from '../../services/api';
|
|
|
import { currentTaskAtom } from '../../atoms/task-atoms';
|
|
|
import { currentProjectAtom } from '../../atoms/project-atoms';
|
|
|
+import { LoadingSpinner } from '../../components/loading-spinner';
|
|
|
import styles from './annotation-view.module.scss';
|
|
|
|
|
|
// Clear localStorage of any LabelStudio:settings as it may cause issues with fullscreen mode
|
|
|
@@ -76,40 +77,71 @@ export const AnnotationView: React.FC = () => {
|
|
|
let LabelStudio: any;
|
|
|
let dependencies: any;
|
|
|
let snapshotDisposer: any;
|
|
|
+ let isCleanedUp = false;
|
|
|
|
|
|
function cleanup() {
|
|
|
- // Clear window.LabelStudio if it exists
|
|
|
- if (typeof window !== 'undefined' && (window as any).LabelStudio) {
|
|
|
- delete (window as any).LabelStudio;
|
|
|
- }
|
|
|
+ if (isCleanedUp) return;
|
|
|
+ isCleanedUp = true;
|
|
|
+
|
|
|
+ console.log('Cleaning up LabelStudio editor...');
|
|
|
|
|
|
- setEditorReady(false);
|
|
|
+ // Dispose snapshot listener first
|
|
|
+ if (snapshotDisposer) {
|
|
|
+ try {
|
|
|
+ snapshotDisposer();
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('Error disposing snapshot:', e);
|
|
|
+ }
|
|
|
+ snapshotDisposer = null;
|
|
|
+ }
|
|
|
|
|
|
- if (lsfInstanceRef.current) {
|
|
|
+ if (snapshotDisposerRef.current) {
|
|
|
try {
|
|
|
- lsfInstanceRef.current.destroy();
|
|
|
- } catch {
|
|
|
- // Ignore cleanup errors in HMR scenarios
|
|
|
+ snapshotDisposerRef.current();
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('Error disposing snapshot ref:', e);
|
|
|
}
|
|
|
- lsfInstanceRef.current = null;
|
|
|
+ snapshotDisposerRef.current = null;
|
|
|
}
|
|
|
|
|
|
+ // Cancel any pending animation frames
|
|
|
if (rafIdRef.current !== null) {
|
|
|
cancelAnimationFrame(rafIdRef.current);
|
|
|
rafIdRef.current = null;
|
|
|
}
|
|
|
|
|
|
- if (snapshotDisposer) {
|
|
|
- snapshotDisposer();
|
|
|
- snapshotDisposer = null;
|
|
|
+ // Destroy LabelStudio instance
|
|
|
+ if (lsfInstanceRef.current) {
|
|
|
+ try {
|
|
|
+ // Give React time to finish rendering before destroying
|
|
|
+ setTimeout(() => {
|
|
|
+ if (lsfInstanceRef.current) {
|
|
|
+ lsfInstanceRef.current.destroy();
|
|
|
+ lsfInstanceRef.current = null;
|
|
|
+ }
|
|
|
+ }, 0);
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('Error destroying LSF instance:', e);
|
|
|
+ lsfInstanceRef.current = null;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- snapshotDisposerRef.current = null;
|
|
|
+ // Clear window.LabelStudio if it exists
|
|
|
+ if (typeof window !== 'undefined' && (window as any).LabelStudio) {
|
|
|
+ delete (window as any).LabelStudio;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Reset state
|
|
|
+ setEditorReady(false);
|
|
|
annotationResultRef.current = null;
|
|
|
+
|
|
|
+ console.log('LabelStudio editor cleaned up');
|
|
|
}
|
|
|
|
|
|
async function loadLSF() {
|
|
|
try {
|
|
|
+ console.log('Loading LabelStudio editor for task:', currentTask.id);
|
|
|
+
|
|
|
// Dynamically import LabelStudio
|
|
|
dependencies = await import('@humansignal/editor');
|
|
|
LabelStudio = dependencies.LabelStudio;
|
|
|
@@ -119,16 +151,23 @@ export const AnnotationView: React.FC = () => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- cleanup();
|
|
|
+ // Don't cleanup here, let the previous effect cleanup handle it
|
|
|
setEditorReady(true);
|
|
|
|
|
|
// Initialize LabelStudio instance
|
|
|
setTimeout(() => {
|
|
|
+ if (isCleanedUp) {
|
|
|
+ console.log('Component was cleaned up, skipping LSF initialization');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
if (!editorContainerRef.current) {
|
|
|
setError('编辑器容器未找到');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ console.log('Initializing LabelStudio instance...');
|
|
|
+
|
|
|
lsfInstanceRef.current = new LabelStudio(editorContainerRef.current, {
|
|
|
config: currentProject.config,
|
|
|
task: {
|
|
|
@@ -161,20 +200,30 @@ export const AnnotationView: React.FC = () => {
|
|
|
},
|
|
|
onStorageInitialized: (LS: any) => {
|
|
|
const initAnnotation = () => {
|
|
|
+ if (isCleanedUp) {
|
|
|
+ console.log('Component was cleaned up, skipping annotation initialization');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const as = LS.annotationStore;
|
|
|
const annotation = as.createAnnotation();
|
|
|
as.selectAnnotation(annotation.id);
|
|
|
|
|
|
if (annotation) {
|
|
|
snapshotDisposer = onSnapshot(annotation, () => {
|
|
|
- annotationResultRef.current = annotation.serializeAnnotation();
|
|
|
+ if (!isCleanedUp) {
|
|
|
+ annotationResultRef.current = annotation.serializeAnnotation();
|
|
|
+ }
|
|
|
});
|
|
|
+ snapshotDisposerRef.current = snapshotDisposer;
|
|
|
}
|
|
|
};
|
|
|
- setTimeout(initAnnotation);
|
|
|
+ setTimeout(initAnnotation, 100);
|
|
|
},
|
|
|
});
|
|
|
- });
|
|
|
+
|
|
|
+ console.log('LabelStudio instance initialized');
|
|
|
+ }, 100);
|
|
|
} catch (err: any) {
|
|
|
console.error('Error loading LabelStudio:', err);
|
|
|
setError(err.message || '初始化编辑器失败');
|
|
|
@@ -188,7 +237,7 @@ export const AnnotationView: React.FC = () => {
|
|
|
return () => {
|
|
|
cleanup();
|
|
|
};
|
|
|
- }, [loading, error, currentTask, currentProject]);
|
|
|
+ }, [id, loading, error, currentTask, currentProject]); // Add 'id' to dependencies
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
if (!currentTask || !id) return;
|
|
|
@@ -198,20 +247,47 @@ export const AnnotationView: React.FC = () => {
|
|
|
setError(null);
|
|
|
|
|
|
// Get annotation result from editor
|
|
|
- const annotationResult = annotationResultRef.current || {};
|
|
|
+ const annotationResult = annotationResultRef.current;
|
|
|
+
|
|
|
+ console.log('Annotation result:', annotationResult);
|
|
|
|
|
|
// Validate annotation result
|
|
|
- if (!annotationResult || Object.keys(annotationResult).length === 0) {
|
|
|
+ if (!annotationResult || typeof annotationResult !== 'object') {
|
|
|
setError('请完成标注后再保存');
|
|
|
setIsSaving(false);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // Extract only the result field if it exists, otherwise use the whole object
|
|
|
+ // LabelStudio's serializeAnnotation() returns an object with various fields
|
|
|
+ // We need to extract just the annotation data
|
|
|
+ let resultData: Record<string, any> = {};
|
|
|
+
|
|
|
+ if (annotationResult.result && Array.isArray(annotationResult.result)) {
|
|
|
+ // Standard LabelStudio format: { result: [...], ... }
|
|
|
+ resultData = { result: annotationResult.result };
|
|
|
+ } else if (Array.isArray(annotationResult)) {
|
|
|
+ // If it's already an array, wrap it
|
|
|
+ resultData = { result: annotationResult };
|
|
|
+ } else {
|
|
|
+ // Use the whole object
|
|
|
+ resultData = annotationResult;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate that we have actual annotation data
|
|
|
+ if (!resultData.result || (Array.isArray(resultData.result) && resultData.result.length === 0)) {
|
|
|
+ setError('请完成标注后再保存(标注结果为空)');
|
|
|
+ setIsSaving(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('Sending annotation data:', resultData);
|
|
|
+
|
|
|
// Create annotation
|
|
|
await createAnnotation({
|
|
|
task_id: id,
|
|
|
user_id: 'current_user', // TODO: Get from auth context
|
|
|
- result: annotationResult,
|
|
|
+ result: resultData,
|
|
|
});
|
|
|
|
|
|
// Calculate new progress (increment by 10%, max 100%)
|
|
|
@@ -226,6 +302,7 @@ export const AnnotationView: React.FC = () => {
|
|
|
// Navigate back to tasks list
|
|
|
navigate('/tasks');
|
|
|
} catch (err: any) {
|
|
|
+ console.error('Save annotation error:', err);
|
|
|
setError(err.message || '保存标注失败');
|
|
|
setIsSaving(false);
|
|
|
}
|
|
|
@@ -237,13 +314,7 @@ export const AnnotationView: React.FC = () => {
|
|
|
};
|
|
|
|
|
|
if (loading) {
|
|
|
- return (
|
|
|
- <div className="flex items-center justify-center h-full">
|
|
|
- <div className="text-center">
|
|
|
- <p className="text-body-medium text-secondary-foreground">加载中...</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- );
|
|
|
+ return <LoadingSpinner size="large" message="加载任务数据..." fullScreen />;
|
|
|
}
|
|
|
|
|
|
if (error) {
|
|
|
@@ -337,11 +408,7 @@ export const AnnotationView: React.FC = () => {
|
|
|
/>
|
|
|
) : (
|
|
|
<div className={styles.loadingContainer}>
|
|
|
- <div className="text-center">
|
|
|
- <p className="text-body-medium text-secondary-foreground">
|
|
|
- 初始化编辑器...
|
|
|
- </p>
|
|
|
- </div>
|
|
|
+ <LoadingSpinner size="large" message="初始化编辑器..." />
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|