/* ========================================================================== MSOS Assistant — offline "find my answer" helper. 100% in-browser: no API, no server, no cost. Lazy-loads a small per-language index and answers with a short snippet + a link to the full page. Dependency-free so it stays CSP-safe (script-src 'self'). ========================================================================== */ (function () { "use strict"; var LANG = (document.documentElement.lang || "en").toLowerCase(); if (LANG === "sl") LANG = "si"; // site uses lang="sl", files use "si" if (["en", "mk", "si"].indexOf(LANG) < 0) LANG = "en"; /* ---- localized UI strings ---- */ var UI = { 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 MSOS and I'll point you to the right page.", found: "Here's what I found:", more: "Read more", none: "I couldn't find a clear answer. Try the Student Welcome Guide, or email info@msosorg.com.", guideUrl: "/en/student-welcome-guide/", disclaimer: "Answers come straight from this website." }, mk: { title: "Прашај MSOS", open: "Прашај MSOS", placeholder: "Постави прашање…", send: "Испрати", close: "Затвори", greeting: "Здраво! 👋 Прашај ме било што за студирање во Словенија или за MSOS и ќе те упатам до вистинската страница.", found: "Еве што најдов:", more: "Прочитај повеќе", none: "Не најдов јасен одговор. Пробај го Водичот за студенти или пиши на info@msosorg.com.", 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 in usmeril te bom na pravo stran.", found: "Tole sem našel:", more: "Preberi več", none: "Nisem našel jasnega odgovora. Poglej Vodnik za študente ali piši na info@msosorg.com.", guideUrl: "/si/student-welcome-guide/", disclaimer: "Odgovori prihajajo neposredno s te spletne strani." } }[LANG]; /* ---- curated short answers for common questions (checked before search). EN is complete; MK/SI seed a few — machine-drafted, PROOFREAD before launch. Add more freely: {k:[keywords], a:"short answer", u:"/lang/page/"} ---- */ var INTENTS = { en: [ { k: ["residence", "permit", "temporary", "stay"], u: "/en/student-welcome-guide/student-residence-permit/", a: "You apply for a temporary residence permit for study (usually before or right after arrival). It takes several documents and steps and is 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 the documents and steps." }, { k: ["cost", "living", "expenses", "budget", "money", "rent"], 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", "room", "student home"], 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"], 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"], u: "/en/student-welcome-guide/", a: "Students can work part-time through student work ('študentsko delo'). The guide covers how it works alongside studies." }, { k: ["boni", "benefits", "discount", "meal", "subsidised"], 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", "fees", "scholarship", "funding", "ad futura"], 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"], 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: ["enrol", "enrolment", "evš", "evs", "apply", "application", "admission"], u: "/en/student-welcome-guide/evs-application-guide/", a: "You apply through the eVŠ portal. The eVŠ guide shows exactly how to submit your application." }, { k: ["arrival", "checklist", "first", "when i arrive", "moving"], 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: ["member", "join", "membership", "become"], 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"], u: "/en/support/", a: "You can support MSOS by bank transfer, sponsorship or partnership. See the Support page for details." }, { k: ["contact", "email", "reach", "get in touch"], u: "/en/legal/", a: "You can reach MSOS at info@msosorg.com. Official details are on the Official information page." } ], mk: [ { k: ["дозвол", "престој", "виза"], u: "/mk/student-welcome-guide/student-residence-permit/", a: "Аплицираш за дозвола за привремен престој за студирање. Потребни се неколку документи и чекори; важи околу 1 година." }, { k: ["член", "зачлен", "приклуч"], u: "/mk/become-a-member/", a: "Можеш да се зачлениш за неколку минути — добредојдени се и студенти и поддржувачи." }, { k: ["поддрш", "донац", "донир"], u: "/mk/support/", a: "Можеш да го поддржиш MSOS преку банкарски трансфер, спонзорство или партнерство. Види ја страницата Поддршка." }, { k: ["сместув", "стан", "дом"], u: "/mk/student-welcome-guide/student-accommodation/", a: "Можеш да бараш студентски домови или приватно сместување. Водичот објаснува како и каде." }, { k: ["контакт", "е-пошта", "мејл"], u: "/mk/legal/", a: "Контактирај го MSOS на info@msosorg.com. Официјалните податоци се на страницата Официјални информации." } ], si: [ { k: ["dovoljenje", "prebivanje", "vizum", "bivanje"], u: "/si/student-welcome-guide/student-residence-permit/", a: "Zaprosiš za dovoljenje za začasno prebivanje zaradi študija. Potrebnih je nekaj dokumentov in korakov; velja približno 1 leto." }, { 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"], u: "/si/support/", a: "MSOS lahko podpreš z bančnim nakazilom, sponzorstvom ali partnerstvom. Glej stran Podpri." }, { k: ["nastanit", "stanovanje", "dom", "soba"], u: "/si/student-welcome-guide/student-accommodation/", a: "Iščeš lahko študentske domove ali zasebne najeme. Vodnik pojasni, kako in kje." }, { k: ["kontakt", "e-pošta", "mail"], u: "/si/legal/", a: "MSOS dosežeš na info@msosorg.com. Uradni podatki so na strani Uradni podatki." } ] }[LANG] || []; /* ---- text normalization: lowercase + fold Latin diacritics (č→c…), keep Cyrillic. Tokenize on non-letter/digit (Unicode-aware). ---- */ 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; } // Question/filler words in EN + MK + SI; removed from queries so the // meaningful terms (e.g. "ohrid", "permit") drive the ranking. 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; })(); /* ---- tiny BM25 search over the lazily-loaded index ---- */ var INDEX = null, DF = null, AVGLEN = 0, ready = false, loading = false; function loadIndex() { if (ready || loading) return Promise.resolve(); loading = true; return fetch("/assistant/index-" + LANG + ".json") .then(function (r) { return r.json(); }) .then(function (data) { INDEX = data; DF = {}; var total = 0; for (var i = 0; i < INDEX.length; i++) { var c = INDEX[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; } } } AVGLEN = total / Math.max(INDEX.length, 1); ready = true; loading = false; }) .catch(function () { loading = false; }); } function search(query, k) { if (!ready) return []; var q0 = tokens(query); if (!q0.length) return []; var N = INDEX.length, k1 = 1.5, b = 0.75, scored = []; // Drop stopwords + very common terms so rare, meaningful words win. var q = q0.filter(function (t) { return !STOP[t] && (!DF[t] || DF[t] / N <= 0.4); }); if (!q.length) q = q0.filter(function (t) { return !STOP[t]; }); if (!q.length) q = q0; for (var i = 0; i < N; i++) { var c = INDEX[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 + (N - DF[term] + 0.5) / (DF[term] + 0.5)); score += idf * (f * (k1 + 1)) / (f + k1 * (1 - b + b * c._len / AVGLEN)); } // boost when the query hits the heading/title var hn = norm(c.h + " " + c.t); for (var h = 0; h < q.length; h++) if (hn.indexOf(q[h]) >= 0) score += 0.6; if (score > 0) scored.push([score, c]); } scored.sort(function (a, b) { return b[0] - a[0]; }); return scored.slice(0, k || 3).map(function (s) { return s[1]; }); } function matchIntent(query) { var q = norm(query), best = null, bestHits = 0; for (var i = 0; i < INTENTS.length; i++) { var hits = 0; for (var j = 0; j < INTENTS[i].k.length; j++) if (q.indexOf(norm(INTENTS[i].k[j])) >= 0) hits++; if (hits > bestHits) { bestHits = hits; best = INTENTS[i]; } } return bestHits > 0 ? best : null; } /* ---- snippet: ~180 chars of a chunk, trimmed to a sentence ---- */ function snippet(c) { var x = c.x; var 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; } /* ========================= UI ========================= */ var esc = function (s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """).replace(/'/g, "'"); }; var root, panel, log, input, launch, opened = false; function bubble(text, who, extraHTML) { var b = document.createElement("div"); b.className = "msa-msg msa-" + who; b.innerHTML = '