PageSourceSearch

https://www.izbori.hr/site/bulb-chat-v1.2.1/js/chatbot.js

js izbori.hr collected 2026-09-24 18:31:47 UTC 16,357 bytes, 463 lines download raw bytes

1
2const BulbMessengerLinks = {
3    privacyPolicyCro: 'https://www.izbori.hr/site/UserDocsImages/5456',
4    privacyPolicyEng: 'https://www.izbori.hr/site/UserDocsImages/5456',
5    disclaimerCro: 'https://www.izbori.hr/site/UserDocsImages/5226',
6    disclaimerEng: 'https://www.izbori.hr/site/en/general-information/ai-virtual-assistant-disclaimer/ai-virtual-assistant-disclaimer-5228/5228',
7}
8
9const BulbMessengerStorageSelectors = {
10    userId: 'bulb-messenger-userId',
11    consentAccepted: 'bulb-messenger-consent-accepted',
12}
13
14const BulbMessengerElementsSelectors = {
15    messengerTag: 'chat-i-messenger-element',
16    messengerId: 'bulb-messenger',
17    messengerStyleClass: 'bulb-messenger-element',
18    popupPanelToggleButtonStyleClass: 'popup-panel-toggle-button',
19    cookieBannerId: 'cookie-inner-wrapper',
20}
21
22class BulbMessengerElements {
23    get messengerTag() {
24        return BulbMessengerElementsSelectors.messengerTag;
25    }
26
27    get messengerId() {
28        return BulbMessengerElementsSelectors.messengerId;
29    }
30
31    get messengerStyleClass() {
32        return BulbMessengerElementsSelectors.messengerStyleClass;
33    }
34
35    get messenger() {
36        return this.#getById(this.messengerId);
37    }
38
39    get messengerParentElement() {
40        return document.body;
41    }
42
43    get popupPanelToggleButton() {
44        return this.#getByClassNames(BulbMessengerElementsSelectors.popupPanelToggleButtonStyleClass);
45    }
46
47    get cookieBanner() {
48        return this.#getById(BulbMessengerElementsSelectors.cookieBannerId);
49    }
50    
51    #getById(id) {
52        return document.getElementById(id);
53    }
54
55    #getByClassNames(...classNames) {
56        const elements = document.getElementsByClassName(classNames.join(' '));
57        return elements.length === 0 ? null : elements[0];
58    }
59}
60
61
62class BulbMessenger {
63    #elements;
64
65    constructor(bulbMessengerElements) {
66        this.#elements = bulbMessengerElements;
67    }
68
69    createMessengerElement(chatMode) {
70        const messenger = document.createElement(this.#elements.messengerTag);
71
72        messenger.id = this.#elements.messengerId;
73        messenger.classList.add(this.#elements.messengerStyleClass);
74        messenger.settings = {
75            messengerId: this.#elements.messengerId,
76            mode: chatMode,
77        };
78
79        return messenger;
80    }
81
82    createMessengerElementAsPopup() {
83        const messengerElement = this.createMessengerElement('popup');
84        this.#elements.messengerParentElement.appendChild(messengerElement);
85    }
86
87    openEndConversationDialog(dialogId) {
88        document.getElementById(dialogId).setAttribute('open', true);
89    }
90
91    closeEndConversationDialog(dialogId) {
92        document.getElementById(dialogId).removeAttribute('open', null);
93    }
94}
95
96class BulbMessengerSDK {
97    setValues(userId, appName, botName, apiUrl, wsUrl, accessToken) {
98        window.chatiSDK.setParams({
99            userId,
100            appName,
101            botName,
102            apiUrl,
103            wsUrl,
104            accessToken
105        });
106    }
107
108    getUserId() {
109        const existingUser = this.#loadUserId();
110
111        if (existingUser) {
112            return existingUser;
113        }
114
115        const randomUserId = self.crypto.randomUUID();
116        this.#saveUserId(randomUserId);
117
118        return randomUserId;
119    }
120
121    closeInteraction() {
122        window.chatiSDK.closeInteraction();
123    }
124
125    setInteractionClosedCallback(callback) {
126        window.chatiSDK.addInteractionClosedListener(callback)
127    }
128
129    openMessengerPanel() {
130        window.chatiSDK.openMessengerPanel();
131    }
132
133    closeMessengerPanel() {
134        window.chatiSDK.closeMessengerPanel();
135    }
136
137    clearUserId() {
138        localStorage.removeItem(BulbMessengerStorageSelectors.userId);
139    }
140
141    #loadUserId() {
142        return localStorage.getItem(BulbMessengerStorageSelectors.userId);
143    }
144
145    #saveUserId(userId) {
146        localStorage.setItem(BulbMessengerStorageSelectors.userId, userId);
147    }
148}
149
150
151class BulbMessengerConsent {
152    static #resolveLinks() {
153        const lang = detectCurrentLanguage();
154        return {
155            privacyPolicy: lang === 'en' ? BulbMessengerLinks.privacyPolicyEng : BulbMessengerLinks.privacyPolicyCro,
156            disclaimer: lang === 'en' ? BulbMessengerLinks.disclaimerEng : BulbMessengerLinks.disclaimerCro,
157        };
158    }
159
160    static hasConsented() {
161        return localStorage.getItem(BulbMessengerStorageSelectors.consentAccepted) === 'true';
162    }
163
164    static setConsented() {
165        localStorage.setItem(BulbMessengerStorageSelectors.consentAccepted, 'true');
166    }
167
168    static injectOverlay(popupPanel) {
169        const links = BulbMessengerConsent.#resolveLinks();
170        const lang = detectCurrentLanguage();
171        const overlay = document.createElement('div');
172        overlay.className = 'bulb-consent-overlay';
173
174        if (lang === 'en') {
175            overlay.innerHTML = `
176                <div class="bulb-consent-text">
177                    <p>The AI virtual assistant provides informational responses based on publicly available data.</p>
178                    <p>The responses do not constitute an official, legally binding position nor do they replace official communication with the competent authority.</p>
179                    <p>Please do not enter personal or sensitive data.</p>
180                    <p>Part of the processing may be carried out by an external AI service outside the European Union.</p>
181                    <p>More information about the processing of personal data is available via the <a class="bulb-consent-link" href="${links.privacyPolicy}" target="_blank" rel="noopener noreferrer">Privacy Policy</a> link.</p>
182                    <p>The terms of use of the AI virtual assistant can be found via the <a class="bulb-consent-link" href="${links.disclaimer}" target="_blank" rel="noopener noreferrer">Disclaimer</a> link.</p>
183                </div>
184                <button class="bulb-consent-button">I understand</button>
185            `;
186        } else {
187            overlay.innerHTML = `
188                <div class="bulb-consent-text">
189                    <p>AI virtualni pomoćnik pruža informativne odgovore na temelju javno dostupnih podataka.</p>
190                    <p>Odgovori ne predstavljaju službeni, pravno obvezujući stav niti zamjenjuju službenu komunikaciju s nadležnim tijelom.</p>
191                    <p>Molimo vas da ne unosite osobne ili osjetljive podatke.</p>
192                    <p>Dio obrade može se provoditi putem vanjske AI usluge izvan Europske unije.</p>
193                    <p>Više informacija o obradi osobnih podataka dostupno je na poveznici <a class="bulb-consent-link" href="${links.privacyPolicy}" target="_blank" rel="noopener noreferrer">Politika privatnosti</a>.</p>
194                    <p>Uvjeti korištenja AI virtualnog pomoćnika nalaze se na poveznici <a class="bulb-consent-link" href="${links.disclaimer}" target="_blank" rel="noopener noreferrer">Izjava o odricanju od odgovornosti</a>.</p>
195                </div>
196                <button class="bulb-consent-button">Razumijem</button>
197            `;
198        }
199
200        overlay.querySelector('.bulb-consent-button').addEventListener('click', () => {
201            BulbMessengerConsent.setConsented();
202            overlay.remove();
203        });
204        popupPanel.appendChild(overlay);
205    }
206
207    static setupOverlay() {
208        if (BulbMessengerConsent.hasConsented()) return;
209
210        const observer = new MutationObserver(() => {
211            const popupPanel = document.querySelector('.popup-panel');
212            if (!popupPanel) return;
213            observer.disconnect();
214            BulbMessengerConsent.injectOverlay(popupPanel);
215        });
216
217        observer.observe(document.body, { childList: true, subtree: true });
218    }
219}
220
221
222const bulbMessengerElements = new BulbMessengerElements();
223const bulbMessenger = new BulbMessenger(bulbMessengerElements);
224const bulbMessengerSdk = new BulbMessengerSDK();
225
226function openEndConversationDialog(dialogId) {
227    bulbMessenger.openEndConversationDialog(dialogId);
228}
229
230function closeEndConversationDialog(dialogId) {
231    bulbMessenger.closeEndConversationDialog(dialogId);
232}
233
234function endConversation(dialogId) {
235    bulbMessengerSdk.closeInteraction();
236    closeEndConversationDialog(dialogId);
237}
238
239function createChatbot() {
240    bulbMessenger.createMessengerElementAsPopup();
241}
242
243function handleConversationClosed() {
244    bulbMessengerSdk.closeMessengerPanel();
245
246    // On conversation close, messenger triggers the reinitialize method
247    // Calling the setValues method od SDK trigger the initialize method
248    // Timeout is needed to properly create a new interaction entry with the initialize method
249    setTimeout(() => {
250        bulbMessengerSdk.clearUserId();
251        setSdkValues();
252    }, 1);
253
254}
255
256function detectCurrentLanguage() {
257    const currentUrl = window.location.href;
258    
259    // Check if URL contains '/en/' pattern which indicates English version
260    if (currentUrl.includes('/en/')) {
261        return 'en';
262    }
263    
264    // Default to Croatian for all other cases
265    return 'hr';
266}
267
268function isA11ySettingActive(cssFileName) {
269    const links = document.querySelectorAll('link[rel="stylesheet"]');
270    return Array.from(links).some(link => 
271        link.href && link.href.includes(cssFileName)
272    );
273}
274
275function isInvertSettingsActive() {
276    return isA11ySettingActive('invert.css');
277}
278
279function isBigFontSettingsActive() {
280    return isA11ySettingActive('big.css');
281}
282
283function setSdkValues() {
284    const userId = bulbMessengerSdk.getUserId();
285    const currentLanguage = detectCurrentLanguage();
286
287    // Select appropriate parameters based on detected language
288    const params = currentLanguage === 'en' ? BulbMessengerParamsEnglish : BulbMessengerParamsCroatian;
289
290    bulbMessengerSdk.setValues(
291        userId,
292        params.appName,
293        params.botName,
294        params.apiUrl,
295        params.wsUrl,
296        params.accessToken
297    );
298}
299
300function initSDK() {
301    setSdkValues();
302    bulbMessengerSdk.setInteractionClosedCallback(handleConversationClosed)
303}
304
305function setupDeferredSdk() {
306    const observer = new MutationObserver(() => {
307        if (!document.querySelector(`${BulbMessengerElementsSelectors.messengerTag} .popup-panel-opened`)) return;
308        observer.disconnect();
309        setTimeout(initSDK, 0);
310    });
311    observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
312}
313
314function initChatbot() {
315    setupDeferredSdk();
316    createChatbot();
317    BulbMessengerConsent.setupOverlay();
318}
319
320function setChatCtaMessage() {
321    const currentLanguage = detectCurrentLanguage();
322    const ctaMessageElement = document.querySelector('.chat-i-cta-message');
323    
324    if (ctaMessageElement) {
325        if (currentLanguage === 'en') {
326            ctaMessageElement.textContent = "Hello, I am a virtual assistant for all election-related topics. How may I assist you?";
327        } else {
328            ctaMessageElement.textContent = "Pozdrav! Ja sam GlasAI, virtualni pomoćnik Državnog izbornog povjerenstva! Kako ti mogu pomoći?";
329        }
330    }
331}
332
333function updateMessengerA11yClasses() {
334    // Remove existing a11y classes from body first
335    document.body.classList.remove('chat-i-a11y-big-font', 'chat-i-a11y-invert');
336    
337    // Add classes to body based on current a11y settings
338    if (isBigFontSettingsActive()) {
339        document.body.classList.add('chat-i-a11y-big-font');
340    }
341    
342    if (isInvertSettingsActive()) {
343        document.body.classList.add('chat-i-a11y-invert');
344    }
345}
346
347function updateDialogButtonLabels() {
348    const currentLanguage = detectCurrentLanguage();
349    
350    const cancelButton = document.querySelector('.bui-messenger-close-conversation-dialog-actions .bui-button[data-variant="secondary"] .bui-button-content span');
351    const endButton = document.querySelector('.bui-messenger-close-conversation-dialog-actions .bui-button[data-variant="danger"] .bui-button-content span');
352    
353    if (cancelButton) {
354        cancelButton.textContent = currentLanguage === 'en' ? 'Cancel' : 'Odustani';
355    }
356    
357    if (endButton) {
358        endButton.textContent = currentLanguage === 'en' ? 'End' : 'Završi';
359    }
360}
361
362class BulbMessengerFooterLinks {
363    static #createLink(href, text) {
364        const link = document.createElement('a');
365        link.href = href;
366        link.textContent = text;
367        link.className = 'bulb-footer-link';
368        link.target = '_blank';
369        link.rel = 'noopener noreferrer';
370        return link;
371    }
372
373    static inject(messageInput) {
374        if (messageInput.querySelector('.bulb-footer-links')) return;
375
376        const lang = detectCurrentLanguage();
377        const container = document.createElement('div');
378        container.className = 'bulb-footer-links';
379
380        container.appendChild(BulbMessengerFooterLinks.#createLink(
381            lang === 'en' ? BulbMessengerLinks.privacyPolicyEng : BulbMessengerLinks.privacyPolicyCro,
382            lang === 'en' ? 'Privacy Policy' : 'Politika privatnosti'
383        ));
384        container.appendChild(BulbMessengerFooterLinks.#createLink(
385            lang === 'en' ? BulbMessengerLinks.disclaimerEng : BulbMessengerLinks.disclaimerCro,
386            lang === 'en' ? 'Disclaimer' : 'Izjava o odricanju od odgovornosti'
387        ));
388
389        messageInput.appendChild(container);
390    }
391
392    static setup() {
393        const observer = new MutationObserver(() => {
394            const messageInput = document.querySelector('bui-message-input');
395            if (!messageInput) return;
396            observer.disconnect();
397            BulbMessengerFooterLinks.inject(messageInput);
398        });
399        observer.observe(document.body, { childList: true, subtree: true });
400    }
401}
402
403// "Call to action" methods
404function openChatPanel() {
405    bulbMessengerSdk.openMessengerPanel();
406}
407
408// Cookie banner positioning
409function positionChatAboveCookieBanner() {
410    // Check if cookie banner exists and is visible
411    if (bulbMessengerElements.cookieBanner && window.getComputedStyle(bulbMessengerElements.cookieBanner).display !== 'none') {
412        const cookieBannerHeight = bulbMessengerElements.cookieBanner.offsetHeight;
413
414        // Set CSS variable on the document root: cookie banner height + 4rem
415        document.documentElement.style.setProperty('--chat-i-popup-panel-toggle-button-margin-bottom', `calc(${cookieBannerHeight}px + 4rem)`);
416    } else {
417        // Cookie banner is hidden or doesn't exist - reset to default value
418        document.documentElement.style.setProperty('--chat-i-popup-panel-toggle-button-margin-bottom', '4rem');
419    }
420}
421
422function initCookieBannerObserver() {
423    if (!bulbMessengerElements.cookieBanner) return;
424    
425    // ResizeObserver to watch for size changes
426    const resizeObserver = new ResizeObserver(() => {
427        positionChatAboveCookieBanner();
428    });
429    
430    resizeObserver.observe(bulbMessengerElements.cookieBanner);
431    
432    // MutationObserver to detect style/class changes (like display: none)
433    const mutationObserver = new MutationObserver(() => {
434        positionChatAboveCookieBanner();
435    });
436    
437    mutationObserver.observe(bulbMessengerElements.cookieBanner, {
438        attributes: true,
439        attributeFilter: ['style', 'class']
440    });
441    
442    // Initial positioning
443    positionChatAboveCookieBanner();
444}
445
446window.addEventListener('load', () => {
447    initChatbot();
448
449    // Set the chat CTA message based on detected language
450    setChatCtaMessage();
451
452    // Update a11y classes based on loaded a11y css files 
453    updateMessengerA11yClasses();
454
455    // Initialize cookie banner observer for chat positioning
456    initCookieBannerObserver();
457
458    // Update dialog button labels based on detected language 
459    updateDialogButtonLabels();
460
461    // Create footer links in message input area
462    BulbMessengerFooterLinks.setup();
463});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.