(function () { var cfg = window.EB_CONCIERGE || {}; var botId = cfg.botId; var apiBase = (cfg.apiBase || '').replace(/\/$/, ''); if (!botId || !apiBase) { // eslint-disable-next-line no-console console.error('[EventBuzz Concierge] Missing window.EB_CONCIERGE.botId or apiBase'); return; } var storageKey = 'ebc_session_' + botId; var sessionId = null; // A page refresh starts a new chat. Remove IDs saved by older widget builds so // participant identity is never silently restored from localStorage. try { localStorage.removeItem(storageKey); } catch {} function el(tag, attrs, children) { var n = document.createElement(tag); if (attrs) for (var k in attrs) n.setAttribute(k, attrs[k]); if (children) children.forEach(function (c) { n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); }); return n; } function addCssOnce() { if (document.getElementById('ebc-css')) return; var link = el('link', { id: 'ebc-css', rel: 'stylesheet', href: apiBase.replace(/\/api$/, '') + '/embed/concierge-widget.css' }); document.head.appendChild(link); } addCssOnce(); var launcher = el('button', { class: 'ebc-launcher', type: 'button', 'aria-label': 'Chat with event concierge' }, ['Chat']); var panel = el('div', { class: 'ebc-panel', role: 'dialog', 'aria-label': 'Event concierge chat' }); var header = el('div', { class: 'ebc-header' }); var title = el('div', { class: 'ebc-title' }, ['Event Concierge']); var headerActions = el('div', { class: 'ebc-header-actions' }); var endBtn = el('button', { class: 'ebc-end', type: 'button', 'aria-label': 'End chat' }, ['End chat']); var closeBtn = el('button', { class: 'ebc-close', type: 'button', 'aria-label': 'Close' }, ['×']); header.appendChild(title); headerActions.appendChild(endBtn); headerActions.appendChild(closeBtn); header.appendChild(headerActions); var messages = el('div', { class: 'ebc-messages' }); var error = el('div', { class: 'ebc-error' }, ['']); var inputBar = el('div', { class: 'ebc-inputbar' }); var input = el('input', { class: 'ebc-input', placeholder: 'Ask a question…', maxlength: '800' }); var sendBtn = el('button', { class: 'ebc-send', type: 'button' }, ['Send']); inputBar.appendChild(input); inputBar.appendChild(sendBtn); panel.appendChild(header); panel.appendChild(messages); panel.appendChild(error); panel.appendChild(inputBar); document.body.appendChild(launcher); document.body.appendChild(panel); function setError(msg) { if (!msg) { error.style.display = 'none'; error.textContent = ''; return; } error.style.display = 'block'; error.textContent = msg; } function parseNumberedRecommendations(text) { var source = String(text || '').trim(); var markerPattern = /(?:^|\s)(\d{1,2})[.)]\s+/g; var markers = []; var match; while ((match = markerPattern.exec(source)) !== null) { markers.push({ number: match[1], markerStart: match.index, contentStart: markerPattern.lastIndex }); } if (markers.length < 2) return null; var intro = source.slice(0, markers[0].markerStart).trim(); var items = markers.map(function (marker, index) { var nextMarker = markers[index + 1]; return { number: marker.number, text: source.slice(marker.contentStart, nextMarker ? nextMarker.markerStart : source.length).trim() }; }).filter(function (item) { return item.text; }); return items.length >= 2 ? { intro: intro, items: items } : null; } function appendAssistantContent(bubble, text) { var parsed = parseNumberedRecommendations(text); if (!parsed) { bubble.textContent = text; return; } bubble.classList.add('ebc-rich'); if (parsed.intro) { bubble.appendChild(el('p', { class: 'ebc-intro' }, [parsed.intro])); } var list = el('div', { class: 'ebc-card-list' }); parsed.items.forEach(function (item) { var card = el('div', { class: 'ebc-card' }); card.appendChild(el('div', { class: 'ebc-card-number' }, [item.number])); card.appendChild(el('div', { class: 'ebc-card-text' }, [item.text])); list.appendChild(card); }); bubble.appendChild(list); } function formatRating(place) { if (place.rating === null || place.rating === undefined || place.rating === '') return 'Rating not available'; var label = String(place.rating); if (place.userRatingsTotal !== null && place.userRatingsTotal !== undefined) { label += ' (' + String(place.userRatingsTotal) + ' reviews)'; } return label; } function mapsUrl(place) { if (place.location && isFinite(Number(place.location.lat)) && isFinite(Number(place.location.lng))) { return 'https://www.google.com/maps/search/?api=1&query=' + encodeURIComponent(String(place.location.lat) + ',' + String(place.location.lng)) + '&query_place_id=' + encodeURIComponent(place.placeId || place.foodId || ''); } return 'https://www.google.com/maps/search/?api=1&query=' + encodeURIComponent((place.name || '') + ' ' + (place.address || '')); } function appendPlaceCards(bubble, text, places) { bubble.classList.add('ebc-rich', 'ebc-places'); if (text) bubble.appendChild(el('p', { class: 'ebc-intro' }, [text])); var list = el('div', { class: 'ebc-place-list' }); places.forEach(function (place) { var card = el('article', { class: 'ebc-place-card' }); var head = el('div', { class: 'ebc-place-head' }); head.appendChild(el('div', { class: 'ebc-place-pin', 'aria-hidden': 'true' }, ['⌖'])); var titleWrap = el('div', { class: 'ebc-place-title-wrap' }); titleWrap.appendChild(el('h3', { class: 'ebc-place-name' }, [place.name || 'Unnamed place'])); titleWrap.appendChild(el('div', { class: 'ebc-place-rating' }, [formatRating(place)])); head.appendChild(titleWrap); card.appendChild(head); if (place.address) card.appendChild(el('p', { class: 'ebc-place-address' }, [place.address])); if (Array.isArray(place.tags) && place.tags.length) { var tags = el('div', { class: 'ebc-place-tags' }); place.tags.slice(0, 3).forEach(function (tag) { tags.appendChild(el('span', { class: 'ebc-place-tag' }, [String(tag)])); }); card.appendChild(tags); } card.appendChild(el('a', { class: 'ebc-place-map', href: mapsUrl(place), target: '_blank', rel: 'noopener noreferrer' }, ['Open map'])); list.appendChild(card); }); bubble.appendChild(list); } function appendFoodCards(bubble, text, foods) { bubble.classList.add('ebc-rich', 'ebc-foods'); if (text) bubble.appendChild(el('p', { class: 'ebc-intro' }, [text])); var list = el('div', { class: 'ebc-food-list' }); foods.forEach(function (food) { var card = el('article', { class: 'ebc-food-card' }); var head = el('div', { class: 'ebc-place-head' }); head.appendChild(el('div', { class: 'ebc-food-icon', 'aria-hidden': 'true' }, ['🍽'])); var titleWrap = el('div', { class: 'ebc-place-title-wrap' }); titleWrap.appendChild(el('h3', { class: 'ebc-place-name' }, [food.name || 'Unnamed restaurant'])); titleWrap.appendChild(el('div', { class: 'ebc-place-rating' }, [formatRating(food)])); head.appendChild(titleWrap); card.appendChild(head); if (food.address) card.appendChild(el('p', { class: 'ebc-place-address' }, [food.address])); if (Array.isArray(food.cuisines) && food.cuisines.length) { var cuisines = el('div', { class: 'ebc-place-tags' }); food.cuisines.slice(0, 3).forEach(function (cuisine) { cuisines.appendChild(el('span', { class: 'ebc-food-tag' }, [String(cuisine).replace(/_/g, ' ')])); }); card.appendChild(cuisines); } card.appendChild(el('a', { class: 'ebc-place-map', href: mapsUrl(food), target: '_blank', rel: 'noopener noreferrer' }, ['Open map'])); list.appendChild(card); }); bubble.appendChild(list); } function addMessage(role, text, places, foods) { var bubble = el('div', { class: 'ebc-msg' + (role === 'user' ? ' ebc-user' : '') }); if (role === 'assistant' && Array.isArray(foods) && foods.length) appendFoodCards(bubble, text, foods); else if (role === 'assistant' && Array.isArray(places) && places.length) appendPlaceCards(bubble, text, places); else if (role === 'assistant') appendAssistantContent(bubble, text); else bubble.textContent = text; messages.appendChild(bubble); messages.scrollTop = messages.scrollHeight; } function openPanel() { panel.classList.add('ebc-open'); input.focus(); } function closePanel() { panel.classList.remove('ebc-open'); } launcher.addEventListener('click', function () { if (panel.classList.contains('ebc-open')) closePanel(); else openPanel(); }); closeBtn.addEventListener('click', closePanel); endBtn.addEventListener('click', function () { var endingSessionId = sessionId; sessionId = null; try { localStorage.removeItem(storageKey); } catch {} messages.textContent = ''; addMessage('assistant', 'This chat has ended. Ask a question to start a new session.'); if (endingSessionId) { http('/public/concierge/' + encodeURIComponent(botId) + '/session/end', { sessionId: endingSessionId }).catch(function () {}); } }); function http(path, body) { return fetch(apiBase + path, { method: body ? 'POST' : 'GET', headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, credentials: 'omit' }).then(function (r) { return r.json().then(function (j) { return { status: r.status, json: j }; }); }); } // Load config http('/public/concierge/' + encodeURIComponent(botId) + '/config').then(function (res) { if (res.json && res.json.success && res.json.data) { title.textContent = res.json.data.eventName ? ('Concierge · ' + res.json.data.eventName) : 'Event Concierge'; var greet = res.json.data.greeting; if (greet) addMessage('assistant', greet); if (res.json.data.maxMessageLength) input.setAttribute('maxlength', String(res.json.data.maxMessageLength)); } }); function send() { var text = String(input.value || '').trim(); if (!text) return; setError(null); input.value = ''; addMessage('user', text); sendBtn.disabled = true; http('/public/concierge/' + encodeURIComponent(botId) + '/chat', { sessionId: sessionId || undefined, message: text }).then(function (res) { sendBtn.disabled = false; if (!res.json || !res.json.success) { setError((res.json && res.json.error && res.json.error.message) ? res.json.error.message : 'Request failed'); return; } if (res.json.data && res.json.data.sessionId) { sessionId = res.json.data.sessionId; } addMessage( 'assistant', (res.json.data && res.json.data.reply) ? res.json.data.reply : '…', res.json.data && Array.isArray(res.json.data.places) ? res.json.data.places : [], res.json.data && Array.isArray(res.json.data.foods) ? res.json.data.foods : [] ); // Log detected intent to browser console when backend returns it if (res.json.data && res.json.data.intent) { // eslint-disable-next-line no-console console.info('[EventBuzz Concierge] detected intent:', res.json.data.intent); } }).catch(function (e) { sendBtn.disabled = false; setError(e && e.message ? e.message : 'Network error'); }); } sendBtn.addEventListener('click', send); input.addEventListener('keydown', function (e) { if (e.key === 'Enter') send(); }); })();