Planeia a tua viagem mais facilmente com a BNO Travel
Encontra alojamento numa seleção de destinos, pesquisa voos para viagens próximas ou longínquas e reserva um carro de aluguer para manteres a liberdade de movimentos. Tudo reunido numa solução clara e fácil de utilizar.
A BNO Travel reúne os elementos essenciais da tua viagem numa única solução, adaptada tanto às necessidades atuais como à sua evolução futura.
Encontra o teu próximo destino
Das montanhas norueguesas e dos Alpes ao sol, às grandes cidades e às viagens longínquas. Explora uma seleção de destinos, encontra alojamento e continua a planear a viagem com voos e carro de aluguer.
Das montanhas norueguesas e suecas aos Alpes. Destinos para esqui, ciclismo, caminhadas, férias em família e experiências na natureza ao longo de todo o ano.
Trysil
Esqui, ciclismo, montanha e atividades para toda a família durante todo o ano.
Uma forma mais clara e flexível de planear a tua viagem
A BNO Travel foi criada para simplificar o planeamento da tua viagem. Com acessos claros a alojamentos, voos e carros de aluguer, tens uma experiência mais fácil de compreender, quer queiras reservar hoje ou explorar possibilidades para uma próxima viagem.
Voos e carros de aluguer para ampliar as tuas opções de viagem
Uma solução criada para evoluir
Aplicação BNO Travel — mantém uma melhor visão geral da tua viagem, mesmo quando estás em movimento
A BNO Travel foi desenvolvida para funcionar igualmente bem no site e na aplicação. Começa a planear onde te for mais conveniente e utiliza a mesma solução para manteres o controlo sobre o resto da viagem.
Planeia facilmente no telemóvel e no computador
Desfruta de uma experiência de viagem mais integrada
Acompanha a tua viagem numa solução concebida para evoluir com as tuas necessidades
Tens um chalé, apartamento ou outro alojamento numa região atrativa? A BNO Travel permite que proprietários e senhorios integrem uma plataforma de viagens profissional, centrada na qualidade da apresentação, na visibilidade e numa experiência clara para os viajantes.
Criamos condições para que alojamentos em destinos selecionados sejam apresentados de forma profissional e credível como parte de uma experiência de viagem integrada. O objetivo é facilitar o contacto com os viajantes certos e dar mais valor à tua oferta num ambiente digital mais eficaz.
';
});
}
const flightActions = [];
if (Array.isArray(data.flightOfferActions)) flightActions.push.apply(flightActions, data.flightOfferActions);
if (data.flightAction) flightActions.push(data.flightAction);
if (!proposal || !proposal.flight) {
flightActions.slice(0, 3).forEach(function (action) {
const info = flightSummary(action && action.offer);
if (!info) return;
html += '
Voo
' + escapeHtml(info.airline || 'Sugestão de voo') + '
' +
(info.route ? '
' + escapeHtml(info.route) + '
' : '') +
(info.price ? '
' + escapeHtml(info.price) + '
' : '') + '
';
});
}
appendRichHtml(html);
}
function normalizeIntentText(value) {
return String(value || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/å/g, 'a').replace(/æ/g, 'ae').replace(/ø/g, 'o');
}
function classifyWebTravelIntent(message) {
const value = normalizeIntentText(message);
const categories = [
/overnatting|hytte|leilighet|hotell|hotel|accommodation|lodging/,
/fly|flight|flybillett|avgang/,
/aktivitet|restaurant|reiseforslag|reiseplan|skiferie|ferie/
].filter(function (pattern) { return pattern.test(value); }).length;
if (/reiseforslag|reiseplan|skiferie|familieferie|sett opp|lag et forslag/.test(value) || categories >= 2) return 'trip_planning';
if (/fly|flight|flybillett|avgang/.test(value)) return 'flights';
if (/overnatting|hytte|leilighet|hotell|hotel|accommodation|lodging/.test(value)) return 'accommodation';
if (/restaurant|middag|lunsj|mat/.test(value)) return 'restaurants';
if (/aktivitet|opplevelse|ski|museum/.test(value)) return 'activities';
return 'general';
}
function extractWebDestinationHint(message) {
const value = normalizeIntentText(message);
const matches = [
['portes-du-soleil', ['portes du soleil','morzine','les gets','avoriaz','chatel']],
['tignes-val-disere', ['tignes','val d isere','val disere']],
['chamonix', ['chamonix']], ['verbier', ['verbier']], ['trysil', ['trysil']],
['salen', ['salen','saelen']], ['stranda', ['stranda','strandafjellet']],
['grand-massif', ['grand massif','les carroz']], ['pays-du-mont-blanc', ['pays du mont blanc']],
['3-vallees', ['3 vallees','trois vallees','three valleys']], ['mallorca', ['mallorca','palma']],
['costa-brava', ['costa brava']], ['kaprun', ['kaprun']], ['zell-am-see', ['zell am see']],
['vemdalen', ['vemdalen']]
];
for (let i = 0; i < matches.length; i += 1) {
if (matches[i][1].some(function (term) { return value.indexOf(term) >= 0; })) return matches[i][0];
}
return null;
}
function showInitialHint() {
if (chatEl.children.length) return;
widget.classList.add('is-start');
chatEl.innerHTML =
'
' +
'Experimenta o Assistente de Viagem BNO com IA ' +
'Escolhe uma sugestão abaixo ou escreve a tua pergunta no campo na parte inferior.' +
'
';
quickPromptsEl.style.display = '';
}
function clearInitialHint() {
widget.classList.remove('is-start');
const hint = chatEl.querySelector('.bno-helper-empty-state');
if (hint) hint.remove();
}
function rememberDestinationFromResponse(data) {
const candidates = [
data && data.tripProposal && data.tripProposal.destination && data.tripProposal.destination.destinationSlug,
data && data.tripProposal && data.tripProposal.destination && data.tripProposal.destination.area,
data && data.searchContext && data.searchContext.params && data.searchContext.params.area,
data && data.detectedDestination
];
for (const candidate of candidates) {
const value = String(candidate || '').trim();
if (value) {
lastDestinationSlug = value.slice(0, 120);
return;
}
}
}
function removeContactForm() {
const card = chatEl.querySelector('[data-bno-contact-card]');
if (card) card.remove();
contactFormOpen = false;
}
function setSupportUiActive() {
humanRowEl.classList.add('is-active');
humanTitleEl.textContent = 'Estás a falar com um membro da nossa equipa';
humanHintEl.textContent = 'Chat direto com o apoio ao cliente da BNO Travel';
humanButtonEl.classList.add('is-active');
humanButtonEl.innerHTML = '✓Estás a falar com a BNO Travel';
humanButtonEl.disabled = true;
quickPromptsEl.style.display = 'none';
queryEl.placeholder = 'Escreve uma mensagem à BNO Travel …';
endSupportBtn.hidden = false;
}
function appendSupportIntro() {
clearInitialHint();
chatEl.insertAdjacentHTML('beforeend',
'
BNO Travel
Estás agora a conversar com a BNO Travel
Escreve em que precisas de ajuda. Quando um membro da equipa de apoio responder, a resposta aparecerá aqui.
Se saíres da página antes de conseguirmos responder, podemos contactar-te através do endereço de e-mail que indicaste.
'
);
scrollBottom();
}
function showSupportContactForm() {
if (supportToken) {
enterSupportMode();
return;
}
if (contactFormOpen) {
const existingName = chatEl.querySelector('[data-bno-contact-name]');
if (existingName) existingName.focus();
return;
}
clearInitialHint();
contactFormOpen = true;
quickPromptsEl.style.display = 'none';
setMainInputEnabled(false);
setStatus('Preenche o nome e o endereço de e-mail para iniciares uma conversa com a BNO Travel.');
chatEl.insertAdjacentHTML('beforeend',
'
' +
'
' +
'Fala com um membro da nossa equipa' +
'
Indica o nome e o endereço de e-mail. Assim, podemos contactar-te se saíres do site antes de conseguirmos responder.
' +
'' +
'
' +
'
'
);
scrollBottom();
setTimeout(function () {
const nameInput = chatEl.querySelector('[data-bno-contact-name]');
const emailInput = chatEl.querySelector('[data-bno-contact-email]');
if (nameInput && !nameInput.value) nameInput.focus();
else if (emailInput) emailInput.focus();
}, 50);
}
function cancelSupportContactForm() {
removeContactForm();
if (!supportMode) quickPromptsEl.style.display = '';
setMainInputEnabled(true);
setStatus('');
if (!chatEl.children.length) showInitialHint();
queryEl.focus();
}
function enterSupportMode() {
removeContactForm();
if (!supportMode) appendSupportIntro();
supportMode = true;
setSupportUiActive();
setMainInputEnabled(true);
setStatus('Chat direto com o apoio ao cliente da BNO Travel', 'success');
if (supportToken) startSupportPolling();
queryEl.focus();
}
function submitSupportContactForm(form) {
const nameInput = form.querySelector('[data-bno-contact-name]');
const emailInput = form.querySelector('[data-bno-contact-email]');
const errorEl = form.querySelector('[data-bno-contact-error]');
const name = String(nameInput && nameInput.value || '').trim();
const email = String(emailInput && emailInput.value || '').trim().toLowerCase();
if (!name) {
errorEl.textContent = 'Introduz o teu nome.';
if (nameInput) nameInput.focus();
return;
}
if (!isValidEmail(email)) {
errorEl.textContent = 'Introduz um endereço de e-mail válido.';
if (emailInput) emailInput.focus();
return;
}
errorEl.textContent = '';
saveSupportProfile({ name: name, email: email });
enterSupportMode();
}
function resetSupportUi() {
supportMode = false;
contactFormOpen = false;
supportToken = '';
supportProfile = { name: '', email: '' };
sessionStorage.removeItem(STORAGE_KEY);
sessionStorage.removeItem(PROFILE_KEY);
seenSupportMessageIds.clear();
if (supportPollTimer) clearInterval(supportPollTimer);
supportPollTimer = null;
setSupportUiIdle();
setMainInputEnabled(true);
setStatus('');
}
function resetWholeConversation() {
resetSupportUi();
travelHistory = [];
lastDestinationSlug = '';
lastPackageOptions = [];
lastPackageVisibleCount = 0;
chatEl.innerHTML = '';
queryEl.value = '';
resizeInput();
showInitialHint();
scrollBottom();
}
async function endSupportConversation() {
const tokenToClose = supportToken;
endSupportBtn.disabled = true;
setStatus('A terminar a conversa …');
try {
if (tokenToClose) {
const response = await fetch(API_BASE + SUPPORT_ENDPOINT + '/conversations/' + encodeURIComponent(tokenToClose), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify({ status: 'closed' })
});
const data = await response.json().catch(function () { return null; });
if (!response.ok || !data || !data.ok) {
throw new Error(data && data.error || 'support_close_failed');
}
}
resetWholeConversation();
setStatus('A conversa terminou. Podes iniciar uma nova conversa.', 'success');
} catch (error) {
console.error('[BNO SUPPORT CLOSE]', error);
setStatus('Não foi possível terminar a conversa neste momento. Tenta novamente.', 'error');
} finally {
endSupportBtn.disabled = false;
}
}
async function sendTravelHelper(message) {
clearInitialHint();
quickPromptsEl.style.display = 'none';
appendUserMessage(message);
setStatus('O Assistente de Viagem BNO com IA está a pensar …');
submitEl.disabled = true;
const language = getLanguage();
const nextHistory = travelHistory.concat([{ role: 'user', text: message }]).slice(-16);
try {
const response = await fetch(API_BASE + TRAVEL_HELPER_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify({
message: message,
lang: language,
replyLanguage: language,
detectedIntent: classifyWebTravelIntent(message),
detectedDestination: extractWebDestinationHint(message),
history: nextHistory,
helperContext: {
replyLanguage: language,
detectedIntent: classifyWebTravelIntent(message),
detectedDestination: extractWebDestinationHint(message),
isTripPlanning: classifyWebTravelIntent(message) === 'trip_planning',
source: 'bno-travel-web',
pageUrl: window.location.href
}
})
});
const data = await response.json().catch(function () { return null; });
if (!response.ok || !data || !data.reply) {
throw new Error(data && data.error || 'travel_helper_failed');
}
rememberDestinationFromResponse(data);
const reply = String(data.reply);
const packageOptions = data.packageAction && Array.isArray(data.packageAction.options) ? data.packageAction.options : [];
travelHistory = nextHistory.concat([{ role: 'assistant', text: reply }]).slice(-16);
if (packageOptions.length) {
lastPackageOptions = packageOptions.slice();
lastPackageVisibleCount = 0;
const narrativeHtml = buildPackageNarrativeHtml(packageOptions);
if (narrativeHtml) appendAssistantRichMessage(narrativeHtml, 'Assistente de Viagem BNO com IA');
else appendAssistantMessage(reply, 'Assistente de Viagem BNO com IA');
} else {
appendAssistantMessage(reply, 'Assistente de Viagem BNO com IA');
}
await renderTravelResponseExtras(data);
setStatus('');
} catch (error) {
console.error('[BNO WEB HELPER]', error);
appendAssistantMessage('Desculpa, o Assistente de Viagem BNO com IA não está disponível neste momento. Ainda podes escolher «Fala com um membro da nossa equipa» para contactar a BNO Travel.', 'Assistente de Viagem BNO com IA');
setStatus('Não foi possível obter uma resposta do Assistente de Viagem BNO.', 'error');
} finally {
submitEl.disabled = false;
}
}
async function createSupportConversation(message) {
if (!supportProfile.name || !isValidEmail(supportProfile.email)) {
throw new Error('support_profile_required');
}
const language = getLanguage();
const response = await fetch(API_BASE + SUPPORT_ENDPOINT + '/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify({
customerName: supportProfile.name,
customerEmail: supportProfile.email,
language: language,
destinationSlug: lastDestinationSlug || null,
pageUrl: window.location.href,
initialMessage: message,
travelHelperContext: {
history: travelHistory.slice(-12),
source: 'bno-travel-web',
pageUrl: window.location.href
}
})
});
const data = await response.json().catch(function () { return null; });
if (!response.ok || !data || !data.ok || !data.data || !data.data.publicToken) {
throw new Error(data && data.error || 'support_create_failed');
}
supportToken = String(data.data.publicToken);
sessionStorage.setItem(STORAGE_KEY, supportToken);
startSupportPolling();
}
async function sendSupportMessage(message) {
clearInitialHint();
appendUserMessage(message);
setStatus('A enviar para a BNO Travel …');
submitEl.disabled = true;
try {
if (!supportToken) {
await createSupportConversation(message);
} else {
const response = await fetch(API_BASE + SUPPORT_ENDPOINT + '/conversations/' + encodeURIComponent(supportToken) + '/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify({ body: message })
});
const data = await response.json().catch(function () { return null; });
if (!response.ok || !data || !data.ok) throw new Error(data && data.error || 'support_message_failed');
}
setStatus('A mensagem foi enviada para a BNO Travel.', 'success');
await pollSupportMessages();
} catch (error) {
console.error('[BNO SUPPORT CHAT]', error);
if (String(error && error.message || '') === 'support_profile_required') {
supportMode = false;
setSupportUiIdle();
showSupportContactForm();
setStatus('É necessário preencher o nome e o endereço de e-mail antes de enviar a mensagem.', 'error');
} else {
setStatus('Não foi possível enviar a mensagem. Tenta novamente.', 'error');
}
} finally {
if (!contactFormOpen) submitEl.disabled = false;
}
}
async function pollSupportMessages() {
if (!supportToken) return;
try {
const response = await fetch(API_BASE + SUPPORT_ENDPOINT + '/conversations/' + encodeURIComponent(supportToken) + '/messages', {
cache: 'no-store'
});
const data = await response.json().catch(function () { return null; });
if (!response.ok || !data || !data.ok) return;
if (data.data && data.data.conversation && data.data.conversation.status === 'closed') {
resetWholeConversation();
setStatus('Este chat de apoio terminou. Podes iniciar uma nova conversa.', 'success');
return;
}
const messages = Array.isArray(data.data && data.data.messages) ? data.data.messages : [];
const staffMessages = messages.filter(function (message) { return message.sender_type === 'staff'; });
staffMessages.forEach(function (message) {
const messageId = String(message.id || '');
if (!messageId || seenSupportMessageIds.has(messageId)) return;
const staffName = firstText(message.sender_name, message.staff_name, message.display_name);
appendAssistantMessage(String(message.body || ''), staffName ? staffName + ', BNO Travel' : 'BNO Travel');
seenSupportMessageIds.add(messageId);
});
} catch (error) {
console.warn('[BNO SUPPORT POLL]', error);
}
}
function startSupportPolling() {
if (supportPollTimer) clearInterval(supportPollTimer);
pollSupportMessages();
supportPollTimer = window.setInterval(pollSupportMessages, 8000);
}
async function handleSend(messageOverride) {
if (contactFormOpen) return;
const message = String(messageOverride || queryEl.value || '').trim();
if (!message) {
setStatus('Escreve primeiro uma mensagem.');
return;
}
queryEl.value = '';
resizeInput();
if (!supportMode) {
const selectedPackageIndex = extractPackageChoiceIndex(message);
if (selectedPackageIndex) {
appendUserMessage(message);
openPackageOption(selectedPackageIndex);
return;
}
if (isMorePackageRequest(message)) {
appendUserMessage(message);
await showNextPackageOptions();
return;
}
}
if (supportMode) await sendSupportMessage(message);
else await sendTravelHelper(message);
}
launcher.addEventListener('click', openHelper);
closeBtn.addEventListener('click', closeHelper);
overlay.addEventListener('click', closeHelper);
humanButtonEl.addEventListener('click', function () { showSupportContactForm(); });
endSupportBtn.addEventListener('click', function () { endSupportConversation(); });
submitEl.addEventListener('click', function () { handleSend(); });
queryEl.addEventListener('input', resizeInput);
queryEl.addEventListener('keydown', function (event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSend();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && widget.classList.contains('is-open')) closeHelper();
});
quickPromptsEl.addEventListener('click', function (event) {
const button = event.target.closest('button[data-prompt]');
if (!button || supportMode) return;
handleSend(button.getAttribute('data-prompt') || '');
});
chatEl.addEventListener('click', function (event) {
const packageButton = event.target.closest('[data-bno-package-option-index]');
if (packageButton && !supportMode) {
event.preventDefault();
const index = Number(packageButton.getAttribute('data-bno-package-option-index'));
openPackageOption(index);
return;
}
const moreButton = event.target.closest('[data-bno-show-more-package]');
if (moreButton && !supportMode) {
event.preventDefault();
moreButton.disabled = true;
showNextPackageOptions();
return;
}
const cancel = event.target.closest('[data-bno-contact-cancel]');
if (cancel) {
event.preventDefault();
cancelSupportContactForm();
}
});
chatEl.addEventListener('submit', function (event) {
const form = event.target.closest('[data-bno-contact-form]');
if (!form) return;
event.preventDefault();
submitSupportContactForm(form);
});
showInitialHint();
// Give the closed BNO Reisehjelper launcher one subtle attention cue on page load.
window.setTimeout(startLauncherAttention, 900);
if (supportToken) {
supportMode = true;
clearInitialHint();
appendSupportIntro();
setSupportUiActive();
setStatus('Chat direto com o apoio ao cliente da BNO Travel', 'success');
startSupportPolling();
}
})();