/* ========================================================================== 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: "Прашај го МСОС", open: "Прашај го МСОС", 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, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); }; 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 = '
' + html + "
"; log.appendChild(b); log.scrollTop = log.scrollHeight; return b; } function linkHTML(url, label) { return '' + esc(label || UI.more) + ' →'; } function answer(query) { if (chipsWrap) chipsWrap.style.display = "none"; bubble(esc(query), "user"); var typing = bubble('', "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) + "
" + linkHTML(intent.u), "bot"); var extra = hits.filter(function (c) { return c.u.replace(/\/$/, "") !== intent.u.replace(/\/$/, ""); })[0]; if (extra) bubble('' + esc(UI.also) + '
' + esc(extra.h) + "
" + linkHTML(extra.u), "bot"); track(query, "curated", lang); return; } if (hits.length) { bubble('' + esc(UI.found) + '', "bot"); hits.forEach(function (c) { bubble("" + esc(c.h) + "
" + esc(snippet(c)) + "
" + linkHTML(c.u), "bot"); }); track(query, "search", lang); } else { var guide = (UIS[lang] || UI).guideUrl; bubble(esc((UIS[lang] || UI).none) + "
" + linkHTML(guide, UI.more), "bot"); track(query, "none", lang); } }); } function renderChips() { chipsWrap = document.createElement("div"); chipsWrap.className = "msa-chips"; chipsWrap.innerHTML = '' + esc(UI.tryLabel) + ""; 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 = '' + '"; 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(); }); // Yield to bottom-anchored overlays (cookie-consent banner, language // banner) so the launcher never sits on top of their action buttons — // important in in-app browsers (Instagram/Facebook) where everything // stacks at the bottom. The pill reappears once they are dismissed. (function () { function overlayUp() { return !!document.querySelector(".msos-cc") || !!document.querySelector(".lang-suggest"); } function sync() { root.classList.toggle("msa-hidden", overlayUp()); } try { new MutationObserver(sync).observe(document.body, { childList: true }); } catch (e) {} sync(); })(); } if (document.readyState !== "loading") build(); else document.addEventListener("DOMContentLoaded", build); })();