Bin
2025-12-16 971a2a12c03b74dd2d7d668b9dbc599f5131bcaf
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
{% load filters %}
 
<script nonce="{{request.csp_nonce}}">
 
  const SERVER_ID = {{ request.server_id| json_dumps_ensure_ascii | safe }};
  const EVENT_NAMESPACE_KEY = "heidi_tips";
  const CACHE_KEY = "heidi_live_tips_collection";
  const CACHE_FETCHED_AT_KEY = "heidi_live_tips_collection_fetched_at";
  const CACHE_STALE_TIME = 1000 * 60 * 60; // 1 hour
  const MAX_TIMEOUT = 5000; // 5 seconds
  const TIP_COLLECTION_NAME = "authPage";
 
  const defaultTipsCollection = {
    authPage: [
      {
        title: "你知道吗?",
        description:
          "您可以同步来自主流云存储提供商的数据,在上传时自动收集新项目进行标注,并将标注结果返回以训练和持续改进模型。",
        link: {
          label: "了解更多",
          url: "https://labelstud.io/guide/storage",
          params: {
            experiment: "login_revamp",
            treatment: "sync_cloud_data",
          },
        },
      },
      {
        title: "你知道吗?",
        description:
          "Label Studio 企业版包含更多功能和自动化工具,以此加快数据标注速度,同时确保最高质量。",
        link: {
          label: "了解更多",
          url: "https://humansignal.com/goenterprise/",
          params: {
            experiment: "login_revamp",
            treatment: "enterprise_platform",
          },
        },
      },
      {
        title: "你知道吗?",
        description:
          "Label Studio 拥有数十种针对所有数据类型的预置模板,您可以使用它们来配置标注界面,从图像分类到情感分析,再到有监督的大语言模型微调。",
        link: {
          label: "查看所有模板",
          url: "https://labelstud.io/templates",
          params: {
            experiment: "login_revamp",
            treatment: "templates",
          },
        },
      },
      {
        title: "你知道吗?",
        description: "Label Studio 现已推出针对小型团队和项目优化的 Starter Cloud 服务。",
        link: {
          label: "了解更多",
          url: "https://humansignal.com/pricing/",
          params: {
            experiment: "login_revamp",
            treatment: "starter_cloud"
          }
        }
      },
    ],
  };
 
  function createURL(url, params) {
    const base = new URL(url);
 
    Object.entries(params ?? {}).forEach(([key, value]) => {
      base.searchParams.set(key, value);
    });
 
    if (SERVER_ID) base.searchParams.set("server_id", SERVER_ID);
 
    return base.toString()
  }
 
  const loadLiveTipsCollection = () => {
    // stale while revalidate - we will return the data present in the cache or the default data and fetch updated data to be put into the cache for the next time this function is called without waiting for the promise.
    const cachedData = localStorage.getItem(CACHE_KEY);
    const fetchedAt = localStorage.getItem(CACHE_FETCHED_AT_KEY);
 
    // Read from local storage if the cachedData is less than CACHE_STALE_TIME milliseconds old
    if (cachedData && fetchedAt && Date.now() - Number.parseInt(fetchedAt) < CACHE_STALE_TIME) {
      return JSON.parse(cachedData);
    }
 
    const abortController = new AbortController();
 
    // Abort the request after MAX_TIMEOUT milliseconds to ensure we won't wait for too long, something might be wrong with the network or it could be an air-gapped instance
    const abortTimeout = setTimeout(abortController.abort, MAX_TIMEOUT);
 
    // Fetch from github raw liveContent.json proxied through the server
    fetch("/heidi-tips", {
      headers: {
        "Cache-Control": "no-cache",
        "Content-Type": "application/json",
      },
      signal: abortController.signal,
    })
      .then(async (response) => {
        if (response.ok) {
          const data = await response.json();
 
          // Cache the fetched content
          localStorage.setItem(CACHE_FETCHED_AT_KEY, String(Date.now()));
          localStorage.setItem(CACHE_KEY, JSON.stringify(data));
        }
      })
      .catch((e) => {
        console.warn("Failed to load live Heidi tips collection", e);
      })
      .finally(() => {
        // Wait until the content is fetched to clear the abort timeout
        // The abort should consider the entire request not just the headers
        clearTimeout(abortTimeout);
      });
 
    // Serve possibly stale cached content
    if (cachedData) {
      return JSON.parse(cachedData);
    }
 
    // Default local content
    return defaultTipsCollection;
  };
 
  function getRandomTip() {
    const tipsCollection = loadLiveTipsCollection();
 
    if (!tipsCollection[TIP_COLLECTION_NAME]) return null;
 
    const tips = tipsCollection[TIP_COLLECTION_NAME];
 
    const index = Math.floor(Math.random() * tips.length);
 
    return tips[index];
  }
 
  function getTipCollectionEvent(collection, event) {
    return `${EVENT_NAMESPACE_KEY}.${collection}.${event}`;
  }
 
  function getTipEvent(collection, tip, event) {
    if (tip.link.params?.experiment && tip.link.params?.treatment) {
      return `${EVENT_NAMESPACE_KEY}.${collection}.${tip.link.params?.experiment}.${tip.link.params?.treatment}.${event}`;
    }
    if (tip.link.params?.experiment) {
      return `${EVENT_NAMESPACE_KEY}.${collection}.${tip.link.params?.experiment}.${event}`;
    }
    if (tip.link.params?.treatment) {
      return `${EVENT_NAMESPACE_KEY}.${collection}.${tip.link.params?.treatment}.${event}`;
    }
 
    return getTipCollectionEvent(collection, event);
  }
 
  function getTipMetadata(tip) {
    // Everything except the experiment and treatment params as those are part of the event name
    const { experiment, treatment, ...rest } = tip.link.params ?? {};
    return {
      ...rest,
      content: tip.description ?? tip.content ?? "",
      title: tip.title,
      href: tip.link.url,
      label: tip.link.label,
    };
  }
 
  document.addEventListener('DOMContentLoaded', function () {
    const _container = document.querySelector('.tips');
    const _title = document.querySelector('.tips .title');
    const _description = document.querySelector('.tips .description');
 
    const selectedTip = getRandomTip();
    if (!selectedTip) {
      // Remove the tips container if there are no tips to show
      _container.remove();
      return;
    }
 
    // Add the click handler to all links inside the tips container that have the data-heidi-tip-link attribute
    // In the future if there are more than one tip, we will need to check which tip was clicked
    _container.addEventListener('click', (event) => {
      // If the clicked element is the link, log the click
      if (event.target.hasAttribute('data-heidi-tip-link')) {
        __lsa(getTipEvent(TIP_COLLECTION_NAME, selectedTip, 'click'), getTipMetadata(selectedTip));
      }
    });
 
    const linkUrl = createURL(selectedTip.link.url, selectedTip.link.params)
 
    _title.innerHTML = selectedTip.title;
    _description.innerHTML = `${selectedTip.description} <a data-heidi-tip-link href="${linkUrl}" target="_blank">${selectedTip.link.label}</a>`;
  });
</script>
 
<div class="tips">
  <div class="title"></div>
  <div class="description"></div>
</div>