{% 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>
|