Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Spinner } from "../../../components";
import { cn } from "../../../utils/bem";
import "./Config.scss";
import { EMPTY_CONFIG } from "./Template";
import { API_CONFIG } from "../../../config/ApiConfig";
import { useAPI } from "../../../providers/ApiProvider";
 
const configClass = cn("configure");
 
// Lazy load Label Studio with a single promise to avoid multiple loads
// and enable as early as possible to load the dependencies once this component is mounted for the first time
let dependencies;
const loadDependencies = async () => {
  if (!dependencies) {
    dependencies = import("@humansignal/editor");
  }
  return dependencies;
};
 
export const Preview = ({ config, data, error, loading, project }) => {
  // @see comment about dependencies above
  loadDependencies();
 
  const [storeReady, setStoreReady] = useState(false);
  const lsf = useRef(null);
  const rootRef = useRef();
  const api = useAPI();
  const projectRef = useRef(project);
  projectRef.current = project;
 
  const currentTask = useMemo(() => {
    return {
      id: 1,
      annotations: [],
      predictions: [],
      data,
    };
  }, [data]);
 
  /**
   * Proxy urls to presign them if storage is connected
   * @param {*} _ LS instance
   * @param {string} url http/https are not proxied and returned as is
   */
  const onPresignUrlForProject = async (_, url) => {
    // if URL is a relative, presigned url (url matches /tasks|projects/:id/resolve/.*) make it absolute
    const presignedUrlPattern = /^\/(?:tasks|projects)\/\d+\/resolve\/?/;
    if (presignedUrlPattern.test(url)) {
      url = new URL(url, document.location.origin).toString();
    }
 
    const parsedUrl = new URL(url);
 
    // return same url if http(s)
    if (["http:", "https:"].includes(parsedUrl.protocol)) return url;
 
    const projectId = projectRef.current.id;
 
    const fileuri = btoa(url);
 
    return api.api.createUrl(API_CONFIG.endpoints.presignUrlForProject, { projectId, fileuri }).url;
  };
 
  const currentConfig = useMemo(() => {
    // empty string causes error in LSF
    return config ?? EMPTY_CONFIG;
  }, [config]);
 
  const initLabelStudio = useCallback(async (config, task) => {
    // wait for dependencies to load, the promise is resolved only once
    // and is started when the component is mounted for the first time
    await loadDependencies();
 
    if (lsf.current || !task.data) return;
 
    try {
      lsf.current = new window.LabelStudio(rootRef.current, {
        config,
        task,
        interfaces: ["side-column"],
        // with SharedStore we should use more late event
        onStorageInitialized(LS) {
          LS.settings.bottomSidePanel = true;
 
          const initAnnotation = () => {
            const as = LS.annotationStore;
            const c = as.createAnnotation();
 
            as.selectAnnotation(c.id);
            setStoreReady(true);
          };
 
          // and even then we need to wait a little even after the store is initialized
          setTimeout(initAnnotation);
        },
      });
 
      lsf.current.on("presignUrlForProject", onPresignUrlForProject);
    } catch (err) {
      console.error(err);
    }
  }, []);
 
  useEffect(() => {
    const opacity = loading || error ? 0.6 : 1;
    // to avoid rerenders and data loss we do it this way
 
    document.getElementById("label-studio").style.opacity = opacity;
  }, [loading, error]);
 
  useEffect(() => {
    initLabelStudio(currentConfig, currentTask).then(() => {
      if (storeReady && lsf.current?.store) {
        const store = lsf.current.store;
 
        store.resetState();
        store.assignTask(currentTask);
        store.assignConfig(currentConfig);
        store.initializeStore(currentTask);
 
        const c = store.annotationStore.addAnnotation({
          userGenerate: true,
        });
 
        store.annotationStore.selectAnnotation(c.id);
        console.log("LSF updated");
      }
    });
  }, [currentConfig, currentTask, storeReady]);
 
  useEffect(() => {
    return () => {
      if (lsf.current) {
        console.info("Destroying LSF");
        lsf.current.destroy();
        lsf.current = null;
      }
    };
  }, []);
 
  return (
    <div className={configClass.elem("preview")}>
      <h3>UI 预览</h3>
      {error && (
        <div className={configClass.elem("preview-error")}>
          <h2>
            {error.detail} {error.id}
          </h2>
          {error.validation_errors?.non_field_errors?.map?.((err) => (
            <p key={err}>{err}</p>
          ))}
          {error.validation_errors?.label_config?.map?.((err) => (
            <p key={err}>{err}</p>
          ))}
          {error.validation_errors?.map?.((err) => (
            <p key={err}>{err}</p>
          ))}
        </div>
      )}
      {!data && loading && <Spinner style={{ width: "100%", height: "50vh" }} />}
      <div id="label-studio" className={configClass.elem("preview-ui")} ref={rootRef} />
    </div>
  );
};