msos/assistant.js

329 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ==========================================================================
MSOS Assistant — offline "find my answer" helper (v2).
100% in-browser: no API, no server, no cost. Detects the language of the
QUESTION (not just the page), lazy-loads that language's index, matches a
curated multilingual answer layer + BM25 search, and links to the page.
Dependency-free so it stays CSP-safe (script-src 'self').
========================================================================== */
(function () {
"use strict";
function normLang(l) { l = (l || "en").toLowerCase(); if (l === "sl") l = "si"; return ["en", "mk", "si"].indexOf(l) < 0 ? "en" : l; }
var PAGE_LANG = normLang(document.documentElement.lang);
/* ---- localized UI strings ---- */
var UIS = {
en: { title: "Ask MSOS", open: "Ask MSOS", placeholder: "Ask a question…", send: "Send", close: "Close",
greeting: "Hi! 👋 Ask me anything about studying in Slovenia or about MSOS — I'll point you to the right page. You can ask in English, Macedonian or Slovene.",
disc: "I'm an automated guide and can make mistakes — for official steps, deadlines and amounts, always check the linked page.",
found: "Here's what I found:", also: "You might also like:", more: "Read more",
none: "I couldn't find a clear answer for that. These might help:", tryLabel: "Popular questions:",
guideUrl: "/en/student-welcome-guide/", disclaimer: "Answers come straight from this website." },
mk: { title: "Прашај MSOS", open: "Прашај MSOS", placeholder: "Постави прашање…", send: "Испрати", close: "Затвори",
greeting: "Здраво! 👋 Прашај ме било што за студирање во Словенија или за MSOS — ќе те упатам до вистинската страница. Можеш да прашаш на македонски, англиски или словенечки.",
disc: "Јас сум автоматски водич и можам да згрешам — за официјални чекори, рокови и износи, секогаш провери ја поврзаната страница.",
found: "Еве што најдов:", also: "Можеби ќе ти користи и:", more: "Прочитај повеќе",
none: "Не најдов јасен одговор. Ова можеби ќе помогне:", tryLabel: "Популарни прашања:",
guideUrl: "/mk/student-welcome-guide/", disclaimer: "Одговорите доаѓаат директно од оваа страница." },
si: { title: "Vprašaj MSOS", open: "Vprašaj MSOS", placeholder: "Zastavi vprašanje…", send: "Pošlji", close: "Zapri",
greeting: "Živjo! 👋 Vprašaj me karkoli o študiju v Sloveniji ali o MSOS — usmeril te bom na pravo stran. Vprašaš lahko v slovenščini, angleščini ali makedonščini.",
disc: "Sem samodejni vodnik in se lahko zmotim — za uradne korake, roke in zneske vedno preveri povezano stran.",
found: "Tole sem našel:", also: "Morda te zanima tudi:", more: "Preberi več",
none: "Nisem našel jasnega odgovora. Morda pomaga to:", tryLabel: "Pogosta vprašanja:",
guideUrl: "/si/student-welcome-guide/", disclaimer: "Odgovori prihajajo neposredno s te spletne strani." }
};
/* ---- suggested starter questions (clickable chips) ---- */
var CHIPS = {
en: ["How do I enrol at a university?", "Residence permit?", "Cost of living in Slovenia?", "How to become a member?"],
mk: ["Како да се запишам на факултет?", "Дозвола за престој?", "Трошоци за живот во Словенија?", "Како да станам член?"],
si: ["Kako se vpišem na fakulteto?", "Dovoljenje za prebivanje?", "Stroški življenja v Sloveniji?", "Kako postanem član?"]
};
/* ---- curated short answers, per language. Keywords are matched as
substrings (so short stems like "запиш" catch "запишам/запишување").
EN is the reference; MK/SI are machine-drafted — PROOFREAD before launch.
Add more anytime: {k:[keywords], a:"short answer", u:"/lang/page/"} ---- */
var INTENTS = {
en: [
{ k: ["enrol", "enroll", "enrolment", "register at", "evš", "evs", "admission", "admissio", "apply to univ", "sign up for", "faculty", "university place"], u: "/en/student-welcome-guide/evs-application-guide/",
a: "You apply through the eVŠ portal. The eVŠ guide walks through creating your application, choosing programmes, and the deadlines." },
{ k: ["residence", "permit", "temporary stay", "stay in slove"], u: "/en/student-welcome-guide/student-residence-permit/",
a: "Non-EU students apply for a temporary residence permit for study — several documents and steps, valid ~1 year, then renewed." },
{ k: ["visa"], u: "/en/student-welcome-guide/student-residence-permit/",
a: "Non-EU students generally need a residence permit for study rather than a short visa. See the residence-permit guide for documents and steps." },
{ k: ["cost", "living", "expenses", "budget", "how much money", "prices"], u: "/en/get-to-know-slovenia/",
a: "Monthly living costs depend on your city and housing. The guide breaks down rent, food, transport and student discounts." },
{ k: ["accommodation", "housing", "dorm", "apartment", "where to live", "room", "student home", "rent"], u: "/en/student-welcome-guide/student-accommodation/",
a: "You can look at student dorms and private rentals. The accommodation guide explains how and where to search." },
{ k: ["english", "slovene", "slovenian language", "in english", "language requirement"], u: "/en/student-welcome-guide/",
a: "Some programmes are in English; many are in Slovene. Learning basic Slovene helps a lot with daily life and admin." },
{ k: ["work", "job", "part-time", "student work", "earn"], u: "/en/student-welcome-guide/",
a: "Students can work part-time through student work ('študentsko delo'). The guide covers how it fits alongside studies." },
{ k: ["boni", "benefit", "discount", "meal", "subsid"], u: "/en/student-welcome-guide/",
a: "Students get subsidised meals ('boni') and many discounts. See the guide for how to register and use them." },
{ k: ["tuition", "fee", "scholarship", "funding", "ad futura", "grant"], u: "/en/student-welcome-guide/application-deadlines/",
a: "Tuition varies by programme and status; scholarships like Ad Futura exist. Check deadlines and funding in the guide." },
{ k: ["bank", "account", "iban", "open a bank"], u: "/en/blog/how-to-open-slovenian-bank-account/",
a: "Opening a Slovenian bank account is straightforward with your documents. The blog post walks through it step by step." },
{ k: ["health", "insurance", "doctor", "medical"], u: "/en/student-welcome-guide/",
a: "You'll need health insurance as a student. The Student Welcome Guide explains how to arrange it after you arrive." },
{ k: ["arrival", "checklist", "when i arrive", "first week", "moving", "just arrived"], u: "/en/student-welcome-guide/arrival-checklist/",
a: "There's a first-weeks arrival checklist (registration, permit, bank, health, etc.) to keep you on track." },
{ k: ["deadline", "when to apply", "dates"], u: "/en/student-welcome-guide/application-deadlines/",
a: "Application deadlines depend on the round and programme. The deadlines page lists the key dates to watch." },
{ k: ["member", "join", "membership", "become a member"], u: "/en/become-a-member/",
a: "You can join in a couple of minutes — students and supporters both welcome. Use the Become a member form." },
{ k: ["support", "donate", "donation", "sponsor", "help you"], u: "/en/support/",
a: "You can support MSOS by bank transfer, sponsorship or partnership. See the Support page for details." },
{ k: ["contact", "email", "reach you", "get in touch", "phone"], u: "/en/legal/",
a: "You can reach MSOS at info@msosorg.com. Official details are on the Official information page." },
{ k: ["ohrid"], u: "/en/get-to-know-macedonia/ohrid-more-than-a-postcard/", a: "Ohrid is a UNESCO lakeside town — old bazaar, churches and Kaneo. The 'Get to know Macedonia' story covers what to see." },
{ k: ["who are you", "what is msos", "about msos", "about you"], u: "/en/about-us/", a: "MSOS is the first Macedonian Student Organisation in Slovenia — support, community and culture for Macedonian students. See About us." }
],
mk: [
{ k: ["запиш", "упис", "факултет", "студи", "evš", "evs", "аплиц", "прием", "универзитет"], u: "/mk/student-welcome-guide/evs-application-guide/",
a: "Аплицираш преку порталот eVŠ. Водичот за eVŠ те води чекор по чекор низ пријавата, изборот на програми и роковите." },
{ k: ["дозвол", "престој", "виза", "бивање"], u: "/mk/student-welcome-guide/student-residence-permit/",
a: "Аплицираш за дозвола за привремен престој за студирање — потребни се неколку документи и чекори; важи околу 1 година." },
{ k: ["трошоц", "живот", "цени", "пари", "буџет"], u: "/mk/get-to-know-slovenia/",
a: "Месечните трошоци зависат од градот и сместувањето. Водичот објаснува киринина, храна, превоз и попусти." },
{ k: ["сместув", "стан", "дом", "соба", "кирија", "живеа"], u: "/mk/student-welcome-guide/student-accommodation/",
a: "Можеш да бараш студентски домови или приватно сместување. Водичот објаснува како и каде да бараш." },
{ k: ["работ", "хонорар", "студентск дело", "заработ"], u: "/mk/student-welcome-guide/",
a: "Студентите можат да работат преку студентско дело. Водичот објаснува како да го усогласиш со студиите." },
{ k: ["бони", "попуст", "оброк", "субвенц", "бенефит"], u: "/mk/student-welcome-guide/",
a: "Студентите добиваат субвенционирани оброци („бони“) и многу попусти. Види во водичот како да ги искористиш." },
{ k: ["стипенд", "школарина", "финанс", "ad futura"], u: "/mk/student-welcome-guide/application-deadlines/",
a: "Школарината зависи од програмата и статусот; постојат стипендии како Ad Futura. Провери ги роковите во водичот." },
{ k: ["банк", "сметка", "iban"], u: "/mk/blog/how-to-open-slovenian-bank-account/",
a: "Отворањето банкарска сметка во Словенија е едноставно со документите. Блог објавата објаснува чекор по чекор." },
{ k: ["рок", "кога да аплиц", "датум"], u: "/mk/student-welcome-guide/application-deadlines/",
a: "Роковите зависат од кругот и програмата. Страницата со рокови ги наведува клучните датуми." },
{ k: ["член", "зачлен", "приклуч"], u: "/mk/become-a-member/",
a: "Можеш да се зачлениш за неколку минути — добредојдени се и студенти и поддржувачи." },
{ k: ["поддрш", "донац", "донир", "спонз"], u: "/mk/support/",
a: "Можеш да го поддржиш MSOS преку банкарски трансфер, спонзорство или партнерство. Види ја страницата Поддршка." },
{ k: ["контакт", "е-пошта", "мејл", "телефон"], u: "/mk/legal/",
a: "Контактирај го MSOS на info@msosorg.com. Официјалните податоци се на страницата Официјални информации." },
{ k: ["охрид"], u: "/mk/get-to-know-macedonia/ohrid-more-than-a-postcard/", a: "Охрид е езерски град под УНЕСКО — стара чаршија, цркви и Канео. Приказната „Запознај ја Македонија“ покажува што да видиш." }
],
si: [
{ k: ["vpis", "vpiš", "fakultet", "študij", "evš", "evs", "prijav", "sprejem", "univerz"], u: "/si/student-welcome-guide/evs-application-guide/",
a: "Prijaviš se prek portala eVŠ. Vodnik za eVŠ te vodi skozi prijavo, izbiro programov in roke." },
{ k: ["dovoljenje", "prebivanje", "vizum", "bivanje"], u: "/si/student-welcome-guide/student-residence-permit/",
a: "Zaprosiš za dovoljenje za začasno prebivanje zaradi študija — nekaj dokumentov in korakov; velja približno 1 leto." },
{ k: ["stroš", "življenj", "cene", "denar", "proračun"], u: "/si/get-to-know-slovenia/",
a: "Mesečni stroški so odvisni od mesta in nastanitve. Vodnik razčleni najemnino, hrano, prevoz in popuste." },
{ k: ["nastanit", "stanovanje", "dom", "soba", "najem", "kje živeti"], u: "/si/student-welcome-guide/student-accommodation/",
a: "Iščeš lahko študentske domove ali zasebne najeme. Vodnik pojasni, kako in kje iskati." },
{ k: ["delo", "služba", "študentsko delo", "zaslužit"], u: "/si/student-welcome-guide/",
a: "Študenti lahko delajo prek študentskega dela. Vodnik pojasni, kako to poteka ob študiju." },
{ k: ["boni", "popust", "obrok", "subvenc"], u: "/si/student-welcome-guide/",
a: "Študenti dobijo subvencionirane obroke (bone) in številne popuste. V vodniku piše, kako jih uporabiš." },
{ k: ["štipend", "šolnin", "financ", "ad futura"], u: "/si/student-welcome-guide/application-deadlines/",
a: "Šolnina je odvisna od programa in statusa; obstajajo štipendije, npr. Ad Futura. Roke preveri v vodniku." },
{ k: ["bank", "račun", "iban"], u: "/si/blog/how-to-open-slovenian-bank-account/",
a: "Odprtje slovenskega bančnega računa je z dokumenti enostavno. Blog objava te vodi korak za korakom." },
{ k: ["rok", "kdaj se prijav", "datum"], u: "/si/student-welcome-guide/application-deadlines/",
a: "Roki so odvisni od kroga in programa. Stran z roki navaja ključne datume." },
{ k: ["član", "včlan", "pridruž"], u: "/si/become-a-member/",
a: "Včlaniš se lahko v nekaj minutah — dobrodošli so študenti in podporniki." },
{ k: ["podpri", "donacij", "donir", "sponz"], u: "/si/support/",
a: "MSOS lahko podpreš z bančnim nakazilom, sponzorstvom ali partnerstvom. Glej stran Podpri." },
{ k: ["kontakt", "e-pošta", "mail", "telefon"], u: "/si/legal/",
a: "MSOS dosežeš na info@msosorg.com. Uradni podatki so na strani Uradni podatki." },
{ k: ["ohrid"], u: "/si/get-to-know-macedonia/ohrid-more-than-a-postcard/", a: "Ohrid je jezersko mesto pod Unescom — stara tržnica, cerkve in Kaneo. Zgodba 'Spoznaj Makedonijo' pokaže, kaj videti." }
]
};
/* ---- text normalization + tokenizer ---- */
function norm(s) { s = (s || "").toLowerCase(); try { s = s.normalize("NFD").replace(/[̀-ͯ]/g, ""); } catch (e) {} return s; }
function tokens(s) { var out = [], m = norm(s).match(/[\p{L}\p{N}]+/gu); if (m) for (var i = 0; i < m.length; i++) if (m[i].length > 1) out.push(m[i]); return out; }
var STOP = (function () {
var w = ("the a an of to in on for and or is are be was were do does did how what when where why who which can could would should will my your our we they it this " +
"that these those with at as by from about into over out up down me you i us them have has had need want please get got give tell show find help hi hello hey thanks " +
"kako kaj kje kdaj zakaj kdo kateri ali je so bo bi na za in ali da li mi me ti to ta te tega se z s v iz pri od do imam imate zelim moram lahko prosim zivjo pozdravljeni " +
"kako sto dali koi koga kade zosto koj koja e se na vo za i ili so da li mi me ti nie toa ova imam sakam treba moze prosim zdravo").split(/\s+/);
var o = {}; for (var i = 0; i < w.length; i++) if (w[i]) o[w[i]] = 1; return o;
})();
/* ---- detect the language of the QUESTION (not just the page) ---- */
function detectLang(text) {
if (/[Ѐ-ӿ]/.test(text)) return "mk"; // Cyrillic → Macedonian
var t = " " + norm(text) + " ";
var siHits = (t.match(/ (vpis|kako|kje|stanovanje|studij|dovoljenje|prebivanje|clan|stroski|zelim|prijav|fakulteto|kaj) /g) || []).length;
var enHits = (t.match(/ (how|what|where|the|residence|permit|university|cost|member|want|apply|study|enrol) /g) || []).length;
if (siHits > enHits && siHits > 0) return "si";
if (enHits > 0) return "en";
return PAGE_LANG; // fall back to the page
}
/* ---- per-language index cache + BM25 ---- */
var CACHE = {}; // lang -> {docs, DF, AVG, N}
function loadIndex(lang) {
if (CACHE[lang]) return Promise.resolve(CACHE[lang]);
return fetch("/assistant/index-" + lang + ".json").then(function (r) { return r.json(); }).then(function (docs) {
var DF = {}, total = 0;
for (var i = 0; i < docs.length; i++) {
var c = docs[i]; c._t = tokens(c.x + " " + c.h + " " + c.t); c._len = c._t.length; total += c._len;
var seen = {}; for (var j = 0; j < c._t.length; j++) { var w = c._t[j]; if (!seen[w]) { seen[w] = 1; DF[w] = (DF[w] || 0) + 1; } }
}
CACHE[lang] = { docs: docs, DF: DF, AVG: total / Math.max(docs.length, 1), N: docs.length };
return CACHE[lang];
}).catch(function () { return null; });
}
function search(ix, query, k) {
if (!ix) return [];
var q0 = tokens(query); if (!q0.length) return [];
var q = q0.filter(function (t) { return !STOP[t] && (!ix.DF[t] || ix.DF[t] / ix.N <= 0.4); });
if (!q.length) q = q0.filter(function (t) { return !STOP[t]; });
if (!q.length) q = q0;
var k1 = 1.5, b = 0.75, scored = [];
for (var i = 0; i < ix.docs.length; i++) {
var c = ix.docs[i], score = 0, tf = {};
for (var j = 0; j < c._t.length; j++) tf[c._t[j]] = (tf[c._t[j]] || 0) + 1;
for (var t = 0; t < q.length; t++) {
var term = q[t], f = tf[term]; if (!f) continue;
var idf = Math.log(1 + (ix.N - ix.DF[term] + 0.5) / (ix.DF[term] + 0.5));
score += idf * (f * (k1 + 1)) / (f + k1 * (1 - b + b * c._len / ix.AVG));
}
var hn = norm(c.h + " " + c.t);
for (var h = 0; h < q.length; h++) if (hn.indexOf(q[h]) >= 0) score += 0.8;
if (score > 0) scored.push([score, c]);
}
scored.sort(function (a, b) { return b[0] - a[0]; });
// de-dupe by URL so we don't show the same page twice
var out = [], seenU = {};
for (var s = 0; s < scored.length && out.length < (k || 3); s++) {
var u = scored[s][1].u; if (seenU[u]) continue; seenU[u] = 1; out.push(scored[s][1]);
}
return out;
}
function matchIntent(lang, query) {
var list = INTENTS[lang] || [], q = norm(query), best = null, bestHits = 0;
for (var i = 0; i < list.length; i++) {
var hits = 0;
for (var j = 0; j < list[i].k.length; j++) if (q.indexOf(norm(list[i].k[j])) >= 0) hits++;
if (hits > bestHits) { bestHits = hits; best = list[i]; }
}
return bestHits > 0 ? best : null;
}
function snippet(c) {
var x = c.x, lead = c.h && x.indexOf(c.h) === 0 ? x.slice(c.h.length).replace(/^[.\s]+/, "") : x;
if (lead.length > 200) lead = lead.slice(0, 200).replace(/\s+\S*$/, "") + "…";
return lead;
}
/* ---- privacy-safe analytics: only if the visitor accepted cookies ---- */
function track(query, resultType, lang) {
try {
if (localStorage.getItem("msos-consent") !== "granted") return;
if (typeof window.gtag !== "function") return;
window.gtag("event", "assistant_query", {
q: String(query).slice(0, 120), result: resultType, q_lang: lang, page_lang: PAGE_LANG
});
} catch (e) {}
}
/* ========================= UI ========================= */
var esc = function (s) { return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;"); };
var UI = UIS[PAGE_LANG];
var root, panel, log, input, launch, chipsWrap, opened = false;
function bubble(html, who) {
var b = document.createElement("div");
b.className = "msa-msg msa-" + who;
b.innerHTML = '<div class="msa-bubble">' + html + "</div>";
log.appendChild(b); log.scrollTop = log.scrollHeight; return b;
}
function linkHTML(url, label) { return '<a class="msa-link" href="' + esc(url) + '">' + esc(label || UI.more) + ' →</a>'; }
function answer(query) {
if (chipsWrap) chipsWrap.style.display = "none";
bubble(esc(query), "user");
var typing = bubble('<span class="msa-typing"><i></i><i></i><i></i></span>', "bot");
var lang = detectLang(query);
loadIndex(lang).then(function (ix) {
typing.remove();
var intent = matchIntent(lang, query);
var hits = search(ix, query, 3);
if (intent) {
bubble(esc(intent.a) + "<br>" + linkHTML(intent.u), "bot");
var extra = hits.filter(function (c) { return c.u.replace(/\/$/, "") !== intent.u.replace(/\/$/, ""); })[0];
if (extra) bubble('<span class="msa-also">' + esc(UI.also) + '</span><br><strong>' + esc(extra.h) + "</strong><br>" + linkHTML(extra.u), "bot");
track(query, "curated", lang); return;
}
if (hits.length) {
bubble('<span class="msa-also">' + esc(UI.found) + '</span>', "bot");
hits.forEach(function (c) { bubble("<strong>" + esc(c.h) + "</strong><br>" + esc(snippet(c)) + "<br>" + linkHTML(c.u), "bot"); });
track(query, "search", lang);
} else {
var guide = (UIS[lang] || UI).guideUrl;
bubble(esc((UIS[lang] || UI).none) + "<br>" + linkHTML(guide, UI.more), "bot");
track(query, "none", lang);
}
});
}
function renderChips() {
chipsWrap = document.createElement("div");
chipsWrap.className = "msa-chips";
chipsWrap.innerHTML = '<span class="msa-chips-label">' + esc(UI.tryLabel) + "</span>";
CHIPS[PAGE_LANG].forEach(function (q) {
var c = document.createElement("button");
c.type = "button"; c.className = "msa-chip"; c.textContent = q;
c.addEventListener("click", function () { answer(q); });
chipsWrap.appendChild(c);
});
log.appendChild(chipsWrap);
}
function open() {
opened = true; root.classList.add("msa-open");
panel.setAttribute("aria-hidden", "false"); launch.setAttribute("aria-expanded", "true");
if (!log.childNodes.length) {
bubble(esc(UI.greeting), "bot");
var note = document.createElement("div");
note.className = "msa-note"; note.textContent = UI.disc;
log.appendChild(note);
renderChips(); loadIndex(PAGE_LANG);
}
setTimeout(function () { input.focus(); }, 60);
}
function close() { opened = false; root.classList.remove("msa-open"); panel.setAttribute("aria-hidden", "true"); launch.setAttribute("aria-expanded", "false"); launch.focus(); }
function submit() { var q = input.value.trim(); if (!q) return; input.value = ""; answer(q); }
function build() {
root = document.createElement("div");
root.className = "msa-root"; root.setAttribute("data-msa", "1");
// avoid overlapping the "Become a member" floating CTA on that page
if (document.querySelector(".mm-sticky-cta")) root.classList.add("msa-left");
root.innerHTML =
'<button class="msa-launch" aria-expanded="false" aria-controls="msa-panel" aria-label="' + esc(UI.open) + '">' +
'<i class="fas fa-comment-dots" aria-hidden="true"></i><span>' + esc(UI.open) + '</span></button>' +
'<section class="msa-panel" id="msa-panel" role="dialog" aria-label="' + esc(UI.title) + '" aria-hidden="true">' +
'<header class="msa-head"><span><i class="fas fa-graduation-cap" aria-hidden="true"></i> ' + esc(UI.title) + '</span>' +
'<button class="msa-x" aria-label="' + esc(UI.close) + '">&times;</button></header>' +
'<div class="msa-log" role="log" aria-live="polite"></div>' +
'<form class="msa-form"><input class="msa-input" type="text" autocomplete="off" placeholder="' + esc(UI.placeholder) + '" aria-label="' + esc(UI.placeholder) + '">' +
'<button class="msa-send" type="submit" aria-label="' + esc(UI.send) + '"><i class="fas fa-paper-plane" aria-hidden="true"></i></button></form>' +
'<p class="msa-foot">' + esc(UI.disclaimer) + "</p>" +
"</section>";
document.body.appendChild(root);
panel = root.querySelector(".msa-panel"); log = root.querySelector(".msa-log");
input = root.querySelector(".msa-input"); launch = root.querySelector(".msa-launch");
launch.addEventListener("click", function () { opened ? close() : open(); });
root.querySelector(".msa-x").addEventListener("click", close);
root.querySelector(".msa-form").addEventListener("submit", function (e) { e.preventDefault(); submit(); });
document.addEventListener("keydown", function (e) { if (e.key === "Escape" && opened) close(); });
}
if (document.readyState !== "loading") build();
else document.addEventListener("DOMContentLoaded", build);
})();