(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('');
listOpen = true;
}
html.push(`- ${renderInlineMarkdown(bullet[1])}
`);
return;
}
closeList();
paragraph.push(trimmed);
});
closeParagraph();
closeList();
return stripAnchorTargets(html.join(''));
}
function setBusy(nextBusy) {
isSending = nextBusy;
input.disabled = nextBusy;
send.disabled = nextBusy;
body.querySelectorAll('.mc-cta button').forEach(button => {
button.disabled = nextBusy;
});
}
function buildPageContextPayload() {
const heading = getPageHeading();
return [
`Language: ${lang}`,
`Current section: ${pageContext.sectionLabel}`,
heading ? `Main heading: ${heading}` : '',
].filter(Boolean).join('\n');
}
async function readQuotaExceededMessage(response) {
let errorText = '';
try {
errorText = await response.text();
} catch (err) {
return {
code: '',
message: QUOTA_FALLBACK_MESSAGE
};
}
if (!errorText) {
return {
code: '',
message: QUOTA_FALLBACK_MESSAGE
};
}
try {
const payload = JSON.parse(errorText);
if (payload && payload.code === 'TOKEN_BALANCE_EXHAUSTED' && typeof payload.message === 'string' && payload.message.trim()) {
return {
code: payload.code,
message: payload.message.trim()
};
}
} catch (err) {
return {
code: '',
message: QUOTA_FALLBACK_MESSAGE
};
}
return {
code: '',
message: QUOTA_FALLBACK_MESSAGE
};
}
async function dispatchToBackend(outboundText, historyText, interaction) {
if (!outboundText || isSending) return;
const requestMeta = Object.assign({
question: historyText || outboundText,
messageType: 'typed',
quickActionLabel: '',
quickActionId: ''
}, interaction);
setBusy(true);
const botDiv = document.createElement('div');
botDiv.className = 'mc-msg mc-bot mc-streaming mc-typing';
botDiv.innerHTML = '';
body.appendChild(botDiv);
scrollBodyToEnd();
try {
const streamUrl = `${MC_CHATBOT.streamUrl}&nonce=${encodeURIComponent(MC_CHATBOT.nonce)}`;
const response = await fetch(streamUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: outboundText,
question: requestMeta.question || historyText || outboundText,
page_context: buildPageContextPayload(),
page_url: currentLocation.href,
page_title: getPageTitle(),
chat_history: chatHistory.slice(-MAX_HISTORY),
lang,
locale: getLocale(),
conversation_id: conversationId,
message_type: requestMeta.messageType,
quick_action_label: requestMeta.quickActionLabel || '',
quick_action_id: requestMeta.quickActionId || ''
})
});
if (response.status === 429 && response.headers.get('x-mc-chatbot-upstream-status') === '429') {
const quotaError = await readQuotaExceededMessage(response);
botDiv.remove();
lastRequestStatus = 'failed';
lastFailureCode = quotaError.code || 'http_429';
addBot(quotaError.message);
return;
}
if (!response.ok || !response.body) {
botDiv.remove();
addBot(L.errorConn);
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let accumulated = '';
let displayed = '';
let queued = '';
let pending = '';
let typing = false;
let streamDone = false;
let typingResolved = false;
let resolveTypingDone;
const typingDone = new Promise(resolve => { resolveTypingDone = resolve; });
const finishTypingIfDone = () => {
if (!streamDone || typing || queued || typingResolved) return;
typingResolved = true;
resolveTypingDone();
};
const pumpTypewriter = () => {
if (!queued) {
typing = false;
finishTypingIfDone();
return;
}
typing = true;
const sliceSize = Math.min(queued.length, 4);
displayed += queued.slice(0, sliceSize);
queued = queued.slice(sliceSize);
botDiv.innerHTML = renderMarkdown(displayed);
scrollBodyToEnd();
setTimeout(pumpTypewriter, 18);
};
const renderToken = (token) => {
if (!token) return;
accumulated += token;
queued += token;
botDiv.classList.remove('mc-typing');
if (!typing) pumpTypewriter();
};
while (true) {
const { done, value } = await reader.read();
if (done) break;
pending += decoder.decode(value, { stream: true });
const lines = pending.split(/\r?\n/);
pending = lines.pop() || '';
lines.forEach(line => renderToken(extractStreamLine(line)));
}
pending += decoder.decode();
if (pending) renderToken(extractStreamLine(pending));
streamDone = true;
finishTypingIfDone();
await typingDone;
botDiv.remove();
if (!accumulated.trim()) {
addBot(L.noReply);
return;
}
chatHistory.push({
inputs: { chat_input: historyText || outboundText },
outputs: { chat_output: accumulated }
});
chatHistory = chatHistory.slice(-MAX_HISTORY);
lastRequestStatus = 'success';
lastFailureCode = '';
appendTranscriptEntry({ type: 'bot_markdown', text: accumulated });
saveState();
addFollowUp();
} catch (err) {
botDiv.remove();
addBot(L.errorConn);
} finally {
setBusy(false);
input.focus();
}
}
async function sendTypedMessage() {
const text = input.value.trim();
if (!text || isSending) return;
markInteracted();
addUser(text);
input.value = '';
await dispatchToBackend(text, text, {
question: text,
messageType: 'typed',
quickActionLabel: '',
quickActionId: ''
});
}
function sendOutboundPrompt(promptText, interaction) {
const requestMeta = Object.assign({
question: promptText,
messageType: 'typed',
quickActionLabel: '',
quickActionId: ''
}, interaction);
return dispatchToBackend(promptText, promptText, requestMeta);
}
input.addEventListener('keydown', event => {
if (event.key === 'Enter') sendTypedMessage();
});
send.onclick = sendTypedMessage;
restoreState(readSavedState());
(function scrollToSection() {
const params = new URLSearchParams(currentLocation.search || window.location.search);
const target = params.get('sezione') || params.get('section');
if (!target) return;
const targetMap = {
orari: ['orari', 'orari e giorni', 'orari e giorni di apertura', 'opening hours', 'hours', 'visiting hours', 'horaires', 'oeffnungszeiten', 'offnungszeiten', 'horarios'],
biglietti: ['biglietti', 'biglietti e prezzi', 'biglietto di ingresso', 'ticket prices', 'tickets', 'ticket', 'billets', 'eintrittskarten', 'entradas'],
'come-arrivare': ['come arrivare', 'come raggiungerci', 'dove siamo', 'getting here', 'how to get here', 'how to reach us', 'directions', 'comment arriver', 'anreise', 'como llegar'],
servizi: ['servizi', 'services', 'services aux visiteurs', 'dienstleistungen', 'servicios'],
contatti: ['contatti', 'contatti e prenotazioni', 'contact us', 'contacts', 'contact', 'kontakte', 'contactos']
};
const normalizedTarget = normalize(target);
const targetKey = Object.keys(targetMap).find(key =>
normalize(key) === normalizedTarget ||
targetMap[key].some(alias => normalize(alias) === normalizedTarget)
);
const keywords = (targetMap[targetKey] || [target]).map(normalize);
const scrollTo = (element) => {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
return true;
};
const matchesText = (text, exactOnly) => {
const normalizedText = normalize(text);
if (!normalizedText || normalizedText.length > 120) return false;
return keywords.some(keyword => (exactOnly ? normalizedText === keyword : normalizedText.includes(keyword)));
};
const elementText = (element) => {
if (/^H[1-6]$/i.test(element.tagName)) return element.textContent || '';
return Array.from(element.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE)
.map(node => node.textContent)
.join(' ');
};
const ignoredChrome = (element) => element.closest('header, nav, footer, [role="navigation"], .menu, .site-header, .site-footer');
const tryScroll = () => {
const scopedRoot = document.querySelector('main') || document.body;
const namedElements = scopedRoot.querySelectorAll('[id], [data-section], [data-anchor], a[name]');
for (const element of namedElements) {
const names = [
element.id,
element.getAttribute('name'),
element.dataset.section,
element.dataset.anchor
].map(normalize);
if (names.some(name => name && keywords.includes(name))) return scrollTo(element);
}
const candidates = scopedRoot.querySelectorAll('h1, h2, h3, h4, h5, h6, .wp-block-heading, p, div, span, strong');
for (const exactOnly of [true, false]) {
for (const element of candidates) {
if (ignoredChrome(element)) continue;
if (matchesText(elementText(element), exactOnly)) return scrollTo(element);
}
}
return false;
};
let attempts = 0;
const run = () => {
if (tryScroll() || attempts >= 12) return;
attempts += 1;
setTimeout(run, 250);
};
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(run, 300);
} else {
document.addEventListener('DOMContentLoaded', () => setTimeout(run, 300));
}
})();
})();
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Museo Gypsotheca Antonio Canova - ECPv6.6.4.2//NONSGML v1.0//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Museo Gypsotheca Antonio Canova
X-ORIGINAL-URL:https://www.museocanova.it
X-WR-CALDESC:Events for Museo Gypsotheca Antonio Canova
REFRESH-INTERVAL;VALUE=DURATION:PT1H
X-Robots-Tag:noindex
X-PUBLISHED-TTL:PT1H
BEGIN:VTIMEZONE
TZID:Europe/Rome
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:20240331T010000
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:20241027T010000
END:STANDARD
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:20250330T010000
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:20251026T010000
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Rome:20240420T000000
DTEND;TZID=Europe/Rome:20250107T000000
DTSTAMP:20260623T234704
CREATED:20240418T193137Z
LAST-MODIFIED:20250624T081709Z
UID:10000407-1713571200-1736208000@www.museocanova.it
SUMMARY:Canova. Quattro Tempi
DESCRIPTION:
URL:https://www.museocanova.it/calendario/canova-quattro-tempi/
CATEGORIES:Mostre
ATTACH;FMTTYPE=image/jpeg:https://www.museocanova.it/2022/wp-content/uploads/2024/04/spina-preview.jpg
END:VEVENT
END:VCALENDAR