(function() { const currentLocation = new URL( (typeof MC_CHATBOT !== 'undefined' && MC_CHATBOT.currentUrl) ? MC_CHATBOT.currentUrl : window.location.href, window.location.href ); const path = currentLocation.pathname; const firstPathPart = path.split('/').filter(Boolean)[0]; const lang = ['en', 'fr'].includes(firstPathPart) ? firstPathPart : 'it'; const STORAGE_KEY = 'mc-chatbot-state-v3'; const STATE_TTL_MS = 12 * 60 * 60 * 1000; const MAX_HISTORY = 6; const QUOTA_FALLBACK_MESSAGE = 'Il servizio chatbot non e disponibile al momento. Riprova piu tardi.'; const LOCALES = { it: { header: 'Museo Canova', inputPlaceholder: 'Scrivi una domanda...', closeTitle: 'Chiudi', openTitle: 'Apri chat', errorConn: 'Errore di connessione.', noReply: 'Nessuna risposta ricevuta.', urls: { info: 'https://www.museocanova.it/visita/informazioni/', events: 'https://www.museocanova.it/agenda/appuntamenti/', exhib: 'https://www.museocanova.it/agenda/mostre/', tours: 'https://www.museocanova.it/visita/visite-guidate/', tickets: 'https://www.ticketlandia.com/m/museo-canova' }, welcomeHome: 'πŸ‘‹ Benvenuto al Museo Canova. Posso aiutarti a organizzare la visita o a trovare informazioni sul museo.', welcomeInfo: 'πŸ“ Posso aiutarti con orari, biglietti e come arrivare.', welcomeAgenda: 'πŸ“… Posso aiutarti a trovare eventi, mostre e visite.', welcomeTours: '🎧 Posso aiutarti a capire quale esperienza e piu adatta.', welcomeContacts: '☎️ Posso aiutarti a trovare i riferimenti utili per contatti e prenotazioni.', welcomeMuseum: 'πŸ›οΈ Posso aiutarti a capire cosa vedere e come organizzare la visita.', flowVisita: 'Perfetto! Ti aiuto a organizzare la visita 😊 Cosa cerchi?', flowEventi: 'πŸ“… Ecco cosa posso recuperare per te:', flowVisite: 'Vuoi scoprire il museo con una guida? Abbiamo diverse tipologie di visita πŸ‘‡', idleRecovery: 'Posso aiutarti con orari, biglietti, visite o eventi 😊', followUpQuestion: 'Ti serve altro?', backToMenu: 'Torna al menu principale', noThanks: 'No grazie', thanks: 'Va bene, resto qui se hai bisogno.', labels: { orari: 'πŸ•’ Orari', biglietti: '🎟️ Biglietti', comeArrivare: 'πŸ“ Come arrivare', eventiMostre: 'πŸ“… Eventi e mostre', visite: '🎧 Visite guidate', orariBigArrivare: '🎟️ Orari, biglietti, come arrivare', eventiProssimi: 'πŸ“… Prossimi eventi speciali', mostreAttive: 'πŸ›οΈ Mostre attive', visiteSpontanee: 'Visite spontanee o tematiche', visiteGruppi: 'Per gruppi', visiteScuole: 'Per scuole', contatti: '☎️ Contatti e prenotazioni', scopriMuseo: 'πŸ›οΈ Cosa sto vedendo in questa sezione' }, prompts: { orari: 'Dammi informazioni complete e pratiche sugli orari e i giorni di apertura del Museo Canova, includendo eventuali variazioni stagionali e il link piu utile per approfondire.', biglietti: 'Spiegami in modo chiaro prezzi, riduzioni, modalita di acquisto e link utile per i biglietti del Museo Canova.', comeArrivare: 'Spiegami in modo pratico come arrivare al Museo Canova, includendo se utile auto, mezzi pubblici, mappa e link utile.', eventi: 'Riassumi in modo utile quali eventi o appuntamenti si possono trovare al Museo Canova e indica come controllare il calendario aggiornato.', mostre: 'Dammi una risposta chiara sulle mostre del Museo Canova e indica il link migliore per vedere quelle attive o aggiornate.', visite: 'Spiegami in modo chiaro le opzioni di visite guidate del Museo Canova, per chi sono adatte e dove trovare i dettagli per prenotare.', visiteGruppi: 'Dammi informazioni complete sulle visite guidate per gruppi del Museo Canova, includendo prenotazione e dettagli utili.', visiteScuole: 'Dammi informazioni complete sulle visite e proposte didattiche per scuole del Museo Canova, includendo come richiedere o prenotare.', contatti: 'Indicami in modo chiaro i contatti e le modalita di prenotazione del Museo Canova, con i riferimenti piu utili presenti sul sito.', scopriMuseo: 'Spiegami in modo semplice e concreto cosa riguarda la pagina o sezione del sito che sto visitando e quali informazioni utili posso trovare qui.' }, sectionLabels: { home: 'homepage', info: 'informazioni visita', agenda: 'agenda', tours: 'visite guidate', contacts: 'contatti e prenotazioni', museum: 'sezione del museo', generic: 'pagina del sito' } }, en: { header: 'Canova Museum', inputPlaceholder: 'Ask a question...', closeTitle: 'Close', openTitle: 'Open chat', errorConn: 'Connection error.', noReply: 'No reply received.', urls: { info: 'https://www.museocanova.it/en/visit/info/', events: 'https://www.museocanova.it/en/exhibition/', exhib: 'https://www.museocanova.it/en/exhibition/', tours: 'https://www.museocanova.it/en/visit/guided-tours/', tickets: 'https://www.ticketlandia.com/m/museo-canova' }, welcomeHome: 'πŸ‘‹ Welcome to the Canova Museum. I can help you plan your visit or find information about the museum.', welcomeInfo: 'πŸ“ I can help with opening hours, tickets and directions.', welcomeAgenda: 'πŸ“… I can help you find events, exhibitions and visits.', welcomeTours: '🎧 I can help you understand which experience fits best.', welcomeContacts: '☎️ I can help you find the right details for contacts and bookings.', welcomeMuseum: 'πŸ›οΈ I can help you understand what to explore and how to plan your visit.', flowVisita: 'Great! Let me help you plan your visit 😊 What are you looking for?', flowEventi: 'πŸ“… Here is what I can look up for you:', flowVisite: 'Would you like to explore the museum with a guide? We offer several types of tours πŸ‘‡', idleRecovery: 'I can help with opening hours, tickets, guided tours or events 😊', followUpQuestion: 'Do you need anything else?', backToMenu: 'Back to main menu', noThanks: 'No thanks', thanks: 'All right, I am here if you need anything.', labels: { orari: 'πŸ•’ Opening hours', biglietti: '🎟️ Tickets', comeArrivare: 'πŸ“ Getting here', eventiMostre: 'πŸ“… Events and exhibitions', visite: '🎧 Guided tours', orariBigArrivare: '🎟️ Hours, tickets, directions', eventiProssimi: 'πŸ“… Upcoming special events', mostreAttive: 'πŸ›οΈ Current exhibitions', visiteSpontanee: 'Walk-in or themed tours', visiteGruppi: 'For groups', visiteScuole: 'For schools', contatti: '☎️ Contacts and bookings', scopriMuseo: 'πŸ›οΈ What this section is about' }, prompts: { orari: 'Give me complete and practical information about the Canova Museum opening hours and opening days, including any seasonal changes and the most useful link for details.', biglietti: 'Explain clearly ticket prices, reductions, purchasing options and the most useful ticket link for the Canova Museum.', comeArrivare: 'Explain in a practical way how to reach the Canova Museum, including car, public transport, map and the most useful link if available.', eventi: 'Summarize clearly what events or special appointments visitors can find at the Canova Museum and how to check the updated calendar.', mostre: 'Give me a clear answer about exhibitions at the Canova Museum and the best link to see current or updated ones.', visite: 'Explain clearly the guided tour options at the Canova Museum, who they are for and where to find booking details.', visiteGruppi: 'Give me complete information about guided tours for groups at the Canova Museum, including booking details and useful practical information.', visiteScuole: 'Give me complete information about school visits and educational activities at the Canova Museum, including how to request or book them.', contatti: 'Show me the Canova Museum contact and booking details in a clear practical way, including the most useful references on the site.', scopriMuseo: 'Explain simply and concretely what the page or section of the site I am viewing is about and which useful information I can find here.' }, sectionLabels: { home: 'homepage', info: 'visitor information', agenda: 'events section', tours: 'guided tours', contacts: 'contacts and bookings', museum: 'museum section', generic: 'site page' } }, fr: { header: 'Musee Canova', inputPlaceholder: 'Posez une question...', closeTitle: 'Fermer', openTitle: 'Ouvrir le chat', errorConn: 'Erreur de connexion.', noReply: 'Aucune reponse recue.', urls: { info: 'https://www.museocanova.it/fr/visita/informazioni/', events: 'https://www.museocanova.it/fr/agenda/appuntamenti/', exhib: 'https://www.museocanova.it/fr/agenda/mostre/', tours: 'https://www.museocanova.it/fr/visita/visite-guidate/', tickets: 'https://www.ticketlandia.com/m/museo-canova' }, welcomeHome: 'πŸ‘‹ Bienvenue au Musee Canova. Je peux vous aider a organiser la visite ou a trouver des informations sur le musee.', welcomeInfo: 'πŸ“ Je peux aider avec les horaires, les billets et l acces.', welcomeAgenda: 'πŸ“… Je peux vous aider a trouver evenements, expositions et visites.', welcomeTours: '🎧 Je peux vous aider a choisir l experience la plus adaptee.', welcomeContacts: '☎️ Je peux vous aider a trouver les bons coordonnees pour contacts et reservations.', welcomeMuseum: 'πŸ›οΈ Je peux vous aider a comprendre quoi explorer et comment organiser la visite.', flowVisita: 'Parfait ! Je vous aide a organiser la visite 😊 Que cherchez-vous ?', flowEventi: 'πŸ“… Voici ce que je peux retrouver pour vous :', flowVisite: 'Vous souhaitez decouvrir le musee avec un guide ? Nous proposons plusieurs types de visites πŸ‘‡', idleRecovery: 'Je peux vous aider avec les horaires, les billets, les visites guidees ou les evenements 😊', followUpQuestion: 'Avez-vous besoin d autre chose ?', backToMenu: 'Retour au menu principal', noThanks: 'Non merci', thanks: 'Tres bien, je reste ici si vous avez besoin.', labels: { orari: 'πŸ•’ Horaires', biglietti: '🎟️ Billets', comeArrivare: 'πŸ“ Comment arriver', eventiMostre: 'πŸ“… Evenements et expositions', visite: '🎧 Visites guidees', orariBigArrivare: '🎟️ Horaires, billets, acces', eventiProssimi: 'πŸ“… Prochains evenements speciaux', mostreAttive: 'πŸ›οΈ Expositions en cours', visiteSpontanee: 'Visites libres ou thematiques', visiteGruppi: 'Pour les groupes', visiteScuole: 'Pour les ecoles', contatti: '☎️ Contacts et reservations', scopriMuseo: 'πŸ›οΈ Cette section du site' }, prompts: { orari: 'Donnez-moi des informations completes et pratiques sur les horaires et jours d ouverture du Musee Canova, avec les variations saisonnieres et le lien le plus utile pour les details.', biglietti: 'Expliquez clairement les prix, reductions, modalites d achat et le lien le plus utile pour les billets du Musee Canova.', comeArrivare: 'Expliquez de facon pratique comment rejoindre le Musee Canova, avec voiture, transports publics, carte et lien utile si disponible.', eventi: 'Resumez clairement quels evenements ou rendez-vous speciaux on peut trouver au Musee Canova et comment verifier le calendrier a jour.', mostre: 'Donnez-moi une reponse claire sur les expositions du Musee Canova et le meilleur lien pour voir celles qui sont en cours ou mises a jour.', visite: 'Expliquez clairement les options de visites guidees du Musee Canova, pour qui elles sont adaptees et ou trouver les details de reservation.', visiteGruppi: 'Donnez-moi des informations completes sur les visites guidees pour groupes du Musee Canova, avec reservation et details utiles.', visiteScuole: 'Donnez-moi des informations completes sur les visites et activites pedagogiques pour ecoles du Musee Canova, avec la facon de demander ou reserver.', contatti: 'Indiquez-moi clairement les contacts et modalites de reservation du Musee Canova, avec les references les plus utiles sur le site.', scopriMuseo: 'Expliquez simplement et concretement a quoi correspond la page ou la section du site que je consulte et quelles informations utiles je peux y trouver.' }, sectionLabels: { home: 'page d accueil', info: 'informations de visite', agenda: 'agenda', tours: 'visites guidees', contacts: 'contacts et reservations', museum: 'section du musee', generic: 'page du site' } } }; const L = LOCALES[lang]; const root = document.getElementById('mc-chatbot-root'); if (!root) return; function normalize(value) { return String(value || '') .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[_-]+/g, ' ') .replace(/\s+/g, ' ') .trim(); } function fill(template) { return String(template || '') .replace(/\{info\}/g, L.urls.info) .replace(/\{events\}/g, L.urls.events) .replace(/\{exhib\}/g, L.urls.exhib) .replace(/\{tours\}/g, L.urls.tours) .replace(/\{tickets\}/g, L.urls.tickets); } function stripAnchorTargets(html) { return String(html || '') .replace(/\s+target=(['"]).*?\1/gi, '') .replace(/\s+rel=(['"]).*?\1/gi, ''); } function getStorage() { try { return window.sessionStorage; } catch (err) { return null; } } function getPageTitle() { return document.title || ''; } function getPageHeading() { const heading = document.querySelector('main h1, article h1, .entry-title, .page-title, h1'); return heading ? (heading.textContent || '').trim() : ''; } function getLocale() { const docLocale = (document.documentElement && document.documentElement.lang) ? document.documentElement.lang : ''; return String(docLocale || lang).trim().toLowerCase(); } function generateConversationId() { if (window.crypto && typeof window.crypto.randomUUID === 'function') { return window.crypto.randomUUID(); } return `mc-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; } function detectPageContext() { const title = getPageTitle(); const heading = getPageHeading(); const signals = normalize([path, title, heading].filter(Boolean).join(' ')); const includesAny = (keywords) => keywords.some(keyword => signals.includes(normalize(keyword))); if ( path === '/' || path === '/en/' || path === '/fr/' || normalize(path) === '' ) { return { key: 'home', sectionLabel: L.sectionLabels.home, welcome: L.welcomeHome, ctas: [ { label: L.labels.orariBigArrivare, actionId: 'flowVisita' }, { label: L.labels.eventiMostre, actionId: 'flowEventi' }, { label: L.labels.visite, actionId: 'flowVisiteGuidate' } ] }; } if ( includesAny([ '/visita/informazioni', '/en/visit/info', 'informazioni visita', 'visitor information', 'info visit', 'horaires', 'opening hours', 'biglietti', 'tickets', 'comment arriver', 'getting here' ]) ) { return { key: 'info', sectionLabel: L.sectionLabels.info, welcome: L.welcomeInfo, ctas: [ { label: L.labels.orari, actionId: 'showOrari' }, { label: L.labels.biglietti, actionId: 'showBiglietti' }, { label: L.labels.comeArrivare, actionId: 'showComeArrivare' } ] }; } if ( includesAny([ '/agenda', '/en/exhibition', 'appuntamenti', 'eventi', 'events', 'exhibition', 'exhibitions', 'mostre', 'expositions' ]) ) { return { key: 'agenda', sectionLabel: L.sectionLabels.agenda, welcome: L.welcomeAgenda, ctas: [ { label: L.labels.eventiProssimi, actionId: 'showEventi' }, { label: L.labels.mostreAttive, actionId: 'showMostre' }, { label: L.labels.visite, actionId: 'showVisiteGuidate' } ] }; } if ( includesAny([ '/visita/visite-guidate', '/en/visit/guided tours', '/fr/visita/visites guidate', 'visite guidate', 'guided tours', 'visites guidees' ]) ) { return { key: 'tours', sectionLabel: L.sectionLabels.tours, welcome: L.welcomeTours, ctas: [ { label: L.labels.visiteSpontanee, actionId: 'showVisiteGuidate' }, { label: L.labels.visiteGruppi, actionId: 'showVisiteGruppi' }, { label: L.labels.visiteScuole, actionId: 'showVisiteScuole' } ] }; } if ( includesAny([ 'contatti', 'contact', 'contacts', 'reservations', 'prenotazioni', 'booking', 'bookings', 'reservation' ]) ) { return { key: 'contacts', sectionLabel: L.sectionLabels.contacts, welcome: L.welcomeContacts, ctas: [ { label: L.labels.contatti, actionId: 'showContatti' }, { label: L.labels.comeArrivare, actionId: 'showComeArrivare' }, { label: L.labels.biglietti, actionId: 'showBiglietti' } ] }; } if ( includesAny([ 'museo', 'museum', 'musee', 'gipsoteca', 'gypsotheca', 'canova', 'collezione', 'collection', 'collections', 'opere', 'works' ]) ) { return { key: 'museum', sectionLabel: L.sectionLabels.museum, welcome: L.welcomeMuseum, ctas: [ { label: L.labels.scopriMuseo, actionId: 'showScopriMuseo' }, { label: L.labels.visite, actionId: 'showVisiteGuidate' }, { label: L.labels.orariBigArrivare, actionId: 'flowVisita' } ] }; } return { key: 'generic', sectionLabel: L.sectionLabels.generic, welcome: L.welcomeHome, ctas: [ { label: L.labels.orariBigArrivare, actionId: 'flowVisita' }, { label: L.labels.eventiMostre, actionId: 'flowEventi' }, { label: L.labels.visite, actionId: 'flowVisiteGuidate' } ] }; } const pageContext = detectPageContext(); root.innerHTML = `
`; const toggle = document.getElementById('mc-toggle'); const win = document.getElementById('mc-window'); const body = document.getElementById('mc-body'); const input = document.getElementById('mc-text'); const send = document.getElementById('mc-send'); let hasInteracted = false; let recoveryTimer = null; let chatHistory = []; let transcript = []; let isSending = false; let nextEntryId = 1; let conversationId = generateConversationId(); let lastRequestStatus = 'idle'; let lastFailureCode = ''; function readSavedState() { const storage = getStorage(); if (!storage) return null; try { const raw = storage.getItem(STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object') return null; if (parsed.savedAt && Date.now() - parsed.savedAt > STATE_TTL_MS) { storage.removeItem(STORAGE_KEY); return null; } return parsed; } catch (err) { return null; } } function saveState() { const storage = getStorage(); if (!storage) return; try { storage.setItem(STORAGE_KEY, JSON.stringify({ transcript, chatHistory: chatHistory.slice(-MAX_HISTORY), hasInteracted, nextEntryId, pageContextKey: pageContext.key, conversationId, lastRequestStatus, lastFailureCode, savedAt: Date.now() })); } catch (err) { // Ignore storage failures. } } function buildTranscriptEntry(entry) { if (!entry || typeof entry !== 'object' || !entry.type) return null; const normalized = { id: Number.isInteger(entry.id) ? entry.id : nextEntryId++, type: entry.type }; if (normalized.id >= nextEntryId) { nextEntryId = normalized.id + 1; } if (entry.purpose) { normalized.purpose = String(entry.purpose); } if (entry.type === 'cta') { normalized.extraClass = entry.extraClass ? String(entry.extraClass) : ''; normalized.buttons = Array.isArray(entry.buttons) ? entry.buttons .map(button => ({ label: String(button.label || ''), actionId: String(button.actionId || '') })) .filter(button => button.label && button.actionId) : []; } else { normalized.text = String(entry.text || ''); } return normalized; } function removeTranscriptEntry(entryId) { transcript = transcript.filter(entry => entry.id !== entryId); } function removeEntriesByPurpose(purpose) { transcript = transcript.filter(entry => entry.purpose !== purpose); body.querySelectorAll(`[data-purpose="${purpose}"]`).forEach(el => el.remove()); saveState(); } function renderTranscriptEntry(entry, options) { const settings = Object.assign({ persist: false, scroll: false }, options); let element = null; if (entry.type === 'bot_html') { element = document.createElement('div'); element.className = 'mc-msg mc-bot'; element.innerHTML = stripAnchorTargets(fill(entry.text)); } else if (entry.type === 'bot_markdown') { element = document.createElement('div'); element.className = 'mc-msg mc-bot'; element.innerHTML = renderMarkdown(entry.text); } else if (entry.type === 'user_text') { element = document.createElement('div'); element.className = 'mc-msg mc-user'; element.textContent = entry.text; } else if (entry.type === 'cta') { element = document.createElement('div'); element.className = entry.extraClass ? `mc-cta ${entry.extraClass}` : 'mc-cta'; entry.buttons.forEach(buttonConfig => { const button = document.createElement('button'); button.type = 'button'; button.textContent = buttonConfig.label; button.dataset.label = buttonConfig.label; button.dataset.actionId = buttonConfig.actionId; bindCtaButton(button); element.appendChild(button); }); } if (!element) return null; element.dataset.entryId = String(entry.id); if (entry.purpose) { element.dataset.purpose = entry.purpose; } body.appendChild(element); if (settings.scroll) { scrollBodyToEnd(); } if (settings.persist) { saveState(); } return element; } function appendTranscriptEntry(entry, options) { const normalized = buildTranscriptEntry(entry); if (!normalized) return null; transcript.push(normalized); return renderTranscriptEntry(normalized, Object.assign({ persist: true, scroll: true }, options)); } function bindCtaButton(button) { const actionId = button.dataset.actionId; const action = ACTIONS[actionId]; if (!action) return; button.onclick = () => { if (isSending) return; const label = button.dataset.label || button.textContent || ''; markInteracted(); addUser(label); const wrap = button.closest('.mc-cta'); if (wrap && wrap.parentNode) { removeTranscriptEntry(Number(wrap.dataset.entryId)); wrap.parentNode.removeChild(wrap); } saveState(); action({ question: label, messageType: 'quick_action', quickActionLabel: label, quickActionId: actionId }); }; } function restoreState(savedState) { if (!savedState || !Array.isArray(savedState.transcript) || savedState.transcript.length === 0) return false; body.innerHTML = ''; transcript = []; nextEntryId = Number.isInteger(savedState.nextEntryId) ? savedState.nextEntryId : 1; savedState.transcript.forEach(entry => { const normalized = buildTranscriptEntry(entry); if (!normalized) return; transcript.push(normalized); renderTranscriptEntry(normalized); }); chatHistory = Array.isArray(savedState.chatHistory) ? savedState.chatHistory.slice(-MAX_HISTORY) : []; hasInteracted = !!savedState.hasInteracted; if (typeof savedState.conversationId === 'string' && savedState.conversationId) { conversationId = savedState.conversationId; } if (typeof savedState.lastRequestStatus === 'string') { lastRequestStatus = savedState.lastRequestStatus; } if (typeof savedState.lastFailureCode === 'string') { lastFailureCode = savedState.lastFailureCode; } scrollBodyToEnd(); return transcript.length > 0; } function clearRecovery() { if (recoveryTimer) { clearTimeout(recoveryTimer); recoveryTimer = null; } } function markInteracted() { hasInteracted = true; clearRecovery(); saveState(); } function scheduleRecovery() { clearRecovery(); if (hasInteracted) return; recoveryTimer = setTimeout(() => { if (!hasInteracted && win.classList.contains('open')) addBot(L.idleRecovery); }, 12000); } function scrollBodyToEnd() { body.scrollTop = body.scrollHeight; } function addBot(text) { appendTranscriptEntry({ type: 'bot_html', text }); } function addUser(text) { appendTranscriptEntry({ type: 'user_text', text }); } function addCTAs(buttons, extraClass, purpose) { appendTranscriptEntry({ type: 'cta', extraClass, purpose, buttons }); } function removeFollowUps() { removeEntriesByPurpose('followup'); } function showWelcome() { addBot(pageContext.welcome); addCTAs(pageContext.ctas); scheduleRecovery(); } function addFollowUp() { removeFollowUps(); appendTranscriptEntry({ type: 'bot_html', text: L.followUpQuestion, purpose: 'followup' }); addCTAs([ { label: L.backToMenu, actionId: 'showWelcome' }, { label: L.noThanks, actionId: 'sayThanks' } ], 'mc-followup', 'followup'); } function flowVisita() { addBot(L.flowVisita); addCTAs([ { label: L.labels.orari, actionId: 'showOrari' }, { label: L.labels.biglietti, actionId: 'showBiglietti' }, { label: L.labels.comeArrivare, actionId: 'showComeArrivare' } ]); } function flowEventi() { addBot(L.flowEventi); addCTAs([ { label: L.labels.eventiProssimi, actionId: 'showEventi' }, { label: L.labels.mostreAttive, actionId: 'showMostre' } ]); } function flowVisiteGuidate() { addBot(L.flowVisite); addCTAs([ { label: L.labels.visiteSpontanee, actionId: 'showVisiteGuidate' }, { label: L.labels.visiteGruppi, actionId: 'showVisiteGruppi' }, { label: L.labels.visiteScuole, actionId: 'showVisiteScuole' } ]); } const ACTIONS = { showWelcome, sayThanks: () => addBot(L.thanks), flowVisita, flowEventi, flowVisiteGuidate, showOrari: interaction => sendOutboundPrompt(L.prompts.orari, interaction), showBiglietti: interaction => sendOutboundPrompt(L.prompts.biglietti, interaction), showComeArrivare: interaction => sendOutboundPrompt(L.prompts.comeArrivare, interaction), showEventi: interaction => sendOutboundPrompt(L.prompts.eventi, interaction), showMostre: interaction => sendOutboundPrompt(L.prompts.mostre, interaction), showVisiteGuidate: interaction => sendOutboundPrompt(L.prompts.visite, interaction), showVisiteGruppi: interaction => sendOutboundPrompt(L.prompts.visiteGruppi, interaction), showVisiteScuole: interaction => sendOutboundPrompt(L.prompts.visiteScuole, interaction), showContatti: interaction => sendOutboundPrompt(L.prompts.contatti, interaction), showScopriMuseo: interaction => sendOutboundPrompt(L.prompts.scopriMuseo, interaction) }; function openChat() { win.classList.add('open'); win.setAttribute('aria-hidden', 'false'); if (body.children.length === 0) showWelcome(); scrollBodyToEnd(); input.focus(); } function closeChat() { win.classList.remove('open'); win.setAttribute('aria-hidden', 'true'); clearRecovery(); } toggle.onclick = () => { if (win.classList.contains('open')) closeChat(); else openChat(); }; document.getElementById('mc-close').onclick = closeChat; function readStreamText(obj) { return obj.chat_output || obj.delta || obj.content || obj.text || obj.answer || obj.reply || obj.message || ''; } function extractStreamLine(line) { const trimmed = line.trim(); if (!trimmed) return ''; const payload = trimmed.startsWith('data:') ? trimmed.slice(5).trim() : trimmed; if (!payload || payload === '[DONE]') return ''; try { return readStreamText(JSON.parse(payload)); } catch (err) { return trimmed.startsWith('data:') ? payload : `${trimmed} `; } } function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function safeUrl(value) { const raw = String(value || '').trim(); if (!raw) return ''; try { const parsed = new URL(raw, window.location.href); if (['http:', 'https:', 'mailto:', 'tel:'].includes(parsed.protocol)) return parsed.href; } catch (err) { if (raw.startsWith('/')) return raw; } return ''; } function renderInlineMarkdown(text) { const tokens = []; let lastIndex = 0; const linkPattern = /\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g; let match; const renderPlainInline = (value) => escapeHtml(value).replace( /(https?:\/\/[^\s<)]+)/g, rawUrl => { const href = safeUrl(rawUrl); return href ? `${escapeHtml(rawUrl)}` : rawUrl; } ); while ((match = linkPattern.exec(text)) !== null) { tokens.push(renderPlainInline(text.slice(lastIndex, match.index))); const href = safeUrl(match[2]); const label = escapeHtml(match[1]); tokens.push( href ? `${label}` : label ); lastIndex = linkPattern.lastIndex; } tokens.push(renderPlainInline(text.slice(lastIndex))); return tokens.join('') .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/(^|[^*])\*([^*]+)\*/g, '$1$2'); } function renderMarkdown(text) { const lines = String(text || '').replace(/\r\n/g, '\n').split('\n'); const html = []; let listOpen = false; let paragraph = []; const closeParagraph = () => { if (!paragraph.length) return; html.push(`

${paragraph.map(renderInlineMarkdown).join('
')}

`); paragraph = []; }; const closeList = () => { if (!listOpen) return; html.push(''); listOpen = false; }; lines.forEach(line => { const trimmed = line.trim(); if (!trimmed) { closeParagraph(); closeList(); return; } const heading = trimmed.match(/^(#{1,3})\s+(.+)$/); if (heading) { closeParagraph(); closeList(); const level = heading[1].length + 2; html.push(`${renderInlineMarkdown(heading[2])}`); return; } const bullet = trimmed.match(/^[-*]\s+(.+)$/); if (bullet) { closeParagraph(); if (!listOpen) { html.push('