EventBuzz_Backend/public/embed/concierge-widget.js

299 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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();
});
})();