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.