@ -1,278 +1,313 @@
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
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 .
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" ;
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" ;
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 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." ,
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." ,
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 и ќе те упатам до вистинската страница." ,
found : "Еве што најдов:" , more : "Прочитај повеќе" ,
none : "Н е најдов јасен одговор. Пробај г о Водичот за студенти или пиши на info@msosorg.com." ,
mk : { title : "Прашај MSOS" , open : "Прашај MSOS" , placeholder : "Постави прашање…" , send : "Испрати" , close : "Затвори" ,
greeting : "Здраво! 👋 Прашај ме било што за студирање во Словенија или за MSOS — ќе те упатам до вистинската страница. Можеш да прашаш на македонски, англиски или словенечки." ,
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 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." ,
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." ,
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." }
} [LANG ] ;
} ;
/ * - - - - c u r a t e d s h o r t a n s w e r s f o r c o m m o n q u e s t i o n s ( c h e c k e d b e f o r e s e a r c h ) .
EN is complete ; MK / SI seed a few — machine - drafted , PROOFREAD before launch .
Add more freely : { k : [ keywords ] , a : "short answer" , u : "/lang/page/" } -- -- * /
/* ---- 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?" ]
} ;
/ * - - - - c u r a t e d s h o r t a n s w e r s , p e r l a n g u a g e . K e y w o r d s a r e m a t c h e d a s
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 : [ "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 : [ "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 the documents and steps." } ,
{ k : [ "cost" , "living" , "expenses" , "budget" , "money" , "rent" ] , u : "/en/get-to-know-slovenia/" ,
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" , " room", "student home "] , u : "/en/student-welcome-guide/student-accommodation/" ,
{ 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 "] , u : "/en/student-welcome-guide/" ,
{ 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" ], u : "/en/student-welcome-guide/" ,
a : "Students can work part-time through student work ('študentsko delo'). The guide covers how it work s alongside studies." } ,
{ k : [ "boni" , "benefit s ", "discount" , "meal" , "subsi dise d"] , u : "/en/student-welcome-guide/" ,
{ 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 fit s alongside studies." } ,
{ k : [ "boni" , "benefit ", "discount" , "meal" , "subsi d"] , 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 s ", "scholarship" , "funding" , "ad futura "] , u : "/en/student-welcome-guide/application-deadlines/" ,
{ 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" ], u : "/en/blog/how-to-open-slovenian-bank-account/" ,
{ 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 : [ " 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/" ,
{ 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 : [ "member" , "join" , "membership" , "become" ] , u : "/en/become-a-member/" ,
{ 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" ] , u : "/en/support/" ,
{ 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" , "get in touch" ] , u : "/en/legal/" ,
a : "You can reach MSOS at info@msosorg.com. Official details are on the Official information page." }
{ 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 : [ "дозвол" , "престој" , "виза" ] , u : "/mk/student-welcome-guide/student-residence-permit/" ,
a : "Аплицираш за дозвола за привремен престој за студирање. Потребни с е неколку документи и чекори; важи околу 1 година." } ,
{ 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/" ,
{ k : [ "поддрш" , "донац" , "донир" , "спонз" ], u : "/mk/support/" ,
a : "Можеш да г о поддржиш MSOS преку банкарски трансфер, спонзорство или партнерство. Види ј а страницата Поддршка." } ,
{ k : [ "сместув" , "стан" , "дом" ] , u : "/mk/student-welcome-guide/student-accommodation/" ,
a : "Можеш да бараш студентски домови или приватно сместување. Водичот објаснува како и каде." } ,
{ k : [ "контакт" , "е -пошта" , "мејл" ] , u : "/mk/legal/" ,
a : "Контактирај г о MSOS на info@msosorg.com. Официјалните податоци с е на страницата Официјални информации." }
{ 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. Potrebnih je nekaj dokumentov in korakov; velja približno 1 leto." } ,
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" ] , u : "/si/support/" ,
{ k : [ "podpri" , "donacij" , "donir" , "sponz" ], 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." }
{ 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." }
]
} [ LANG ] || [ ] ;
} ;
/* ---- 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 ; }
/ * - - - - t e x t n o r m a l i z a t i o n : l o w e r c a s e + f o l d L a t i n d i a c r i t i c s ( č → 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 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 ; } ) ;
/* ---- 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
}
function search ( query , k ) {
if ( ! ready ) return [ ] ;
/* ---- 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 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 ) ; } ) ;
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 ;
for ( var i = 0 ; i < N ; i ++ ) {
var c = INDEX [ i ] , score = 0 , tf = { } ;
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 + ( N - DF [ term ] + 0.5 ) / ( DF [ term ] + 0.5 ) ) ;
score += idf * ( f * ( k1 + 1 ) ) / ( f + k1 * ( 1 - b + b * c . _len / AVGLEN ) ) ;
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) ) ;
}
// 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 ;
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 ] ; } ) ;
return scored . slice ( 0 , k || 3 ) . map ( function ( s ) { return s [ 1 ] ; } ) ;
// 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 ( query ) {
var q = norm ( query ) , best = null , bestHits = 0 ;
for ( var i = 0 ; i < INTENTS . length ; i ++ ) {
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 < INTENTS [ i ] . k . length ; j ++ )
if ( q . indexOf ( norm ( INTENTS [ i ] . k [ j ] ) ) >= 0 ) hits ++ ;
if ( hits > bestHits ) { bestHits = hits ; best = INTENTS [ i ] ; }
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 ;
}
/* ---- 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 ;
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 ;
}
/* ========================= UI ========================= */
var esc = function ( s ) { return String ( s == null ? "" : s )
. replace ( /&/g , "&" ) . 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 = '<div class="msa-bubble">' + text + ( extraHTML || "" ) + "</div>" ;
log . appendChild ( b ) ;
log . scrollTop = log . scrollHeight ;
return b ;
/* ---- 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 , """ ) . 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 = '<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 ( "…" , "bot" ) ;
Promise . resolve ( ready ? null : loadIndex ( ) ) . then ( function ( ) {
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 ( query ) ;
var hits = search ( query , 3 ) ;
var intent = matchIntent ( lang, query) ;
var hits = search ( ix, query, 3 ) ;
if ( intent ) {
var extra = '<a class="msa-link" href="' + esc ( intent . u ) + '">' + esc ( UI . more ) + ' →</a>' ;
bubble ( esc ( intent . a ) , "bot" , extra ) ;
// offer one more related page from search if different
var extraHit = hits . filter ( function ( c ) { return c . u !== intent . u ; } ) [ 0 ] ;
if ( extraHit ) bubble ( '<span class="msa-also">' + esc ( UI . found ) + '</span> ' +
esc ( extraHit . h ) + ' <a class="msa-link" href="' + esc ( extraHit . u ) + '">' + esc ( UI . more ) + ' →</a>' , "bot" ) ;
return ;
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><a class="msa-link" href="' + esc ( c . u ) + '">' + esc ( UI . more ) + ' →</a>' , "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 {
bubble ( esc ( UI . none ) + ' <a class="msa-link" href="' + esc ( UI . guideUrl ) + '">' + esc ( UI . more ) + ' →</a>' , "bot" ) ;
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" ) ;
loadIndex ( ) ;
if ( ! log . childNodes . length ) bubble ( esc ( UI . greeting ) , "bot" ) ;
setTimeout ( function ( ) { input . focus ( ) ; } , 50 ) ;
}
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 ) ;
panel . setAttribute ( "aria-hidden" , "false" ) ; launch . setAttribute ( "aria-expanded" , "true" ) ;
if ( ! log . childNodes . length ) { bubble ( esc ( UI . greeting ) , "bot" ) ; 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> ' + esc ( UI . title ) + '</span>' +
'<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 ) + '">×</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>' ;
'<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" ) ;
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 ( ) ; } ) ;