Encuentra alojamiento en una selección de destinos, busca vuelos para viajes cercanos o lejanos y reserva un coche de alquiler para mantener la libertad de movimiento. Todo reunido en una solución clara y fácil de usar.
Decisiones más sencillas. Una mejor visión de conjunto.
BNO Travel reúne los elementos esenciales de tu viaje en una sola solución, adaptada tanto a las necesidades actuales como a su evolución futura.
Encuentra tu próximo destino
Desde las montañas noruegas y los Alpes hasta el sol, las grandes ciudades y los viajes lejanos. Descubre una selección de destinos, encuentra alojamiento y sigue planificando tu viaje con vuelos y coche de alquiler.
Destinos en los que BNO Travel ofrece una selección especialmente amplia de alojamientos, desde Escandinavia y los Alpes hasta el Mediterráneo y Canadá.
Destacado
Trysil
Esquí, montaña, experiencias en familia y actividades durante todo el año en uno de los destinos de montaña más populares de Noruega.
Desde las montañas noruegas y suecas hasta los Alpes. Destinos para esquiar, montar en bicicleta, hacer senderismo, disfrutar de vacaciones en familia y vivir la naturaleza durante todo el año.
Trysil
Esquí, bicicleta, montaña y actividades para toda la familia durante todo el año.
Descubre una cuidada selección de alojamientos en regiones y destinos populares, adecuados tanto para estancias cortas como para vacaciones prolongadas.
Una forma más clara y flexible de planificar tu viaje
BNO Travel está diseñado para simplificar la planificación de tu viaje. Con accesos claros a alojamientos, vuelos y coches de alquiler, disfrutas de una experiencia más fácil de entender, tanto si quieres reservar hoy como explorar posibilidades para un próximo viaje.
Vuelos y coches de alquiler para ampliar tus opciones de viaje
Una solución diseñada para evolucionar
Aplicación BNO Travel – Mantén una mejor visión de conjunto de tu viaje, también cuando estés en movimiento
BNO Travel está diseñado para funcionar igual de bien en la web y en la aplicación. Empieza a planificar donde te resulte más cómodo y utiliza la misma solución para mantener el control sobre el resto del viaje.
Planifica fácilmente desde móvil y ordenador
Disfruta de una experiencia de viaje más integrada
Sigue tu viaje en una solución diseñada para evolucionar con tus necesidades
¿Tienes un chalet, un apartamento u otro alojamiento en una región atractiva? BNO Travel permite a propietarios y arrendadores incorporarse a una plataforma de viajes profesional centrada en la calidad de la presentación, la visibilidad y una experiencia clara para los viajeros.
Permitimos que los alojamientos de destinos seleccionados se presenten de forma profesional y fiable como parte de una experiencia de viaje global. El objetivo es facilitar el encuentro con los viajeros adecuados y dar mayor valor a tu oferta en un entorno digital más 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 += '
Vuelo
' + escapeHtml(info.airline || 'Sugerencia de vuelo') + '
' +
(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 =
'
' +
'Prueba el Asistente de Viaje BNO con IA ' +
'Elige una sugerencia de abajo o escribe tu pregunta en el campo de la 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 chateando con un miembro de nuestro equipo';
humanHintEl.textContent = 'Chat directo con el equipo de atención de BNO Travel';
humanButtonEl.classList.add('is-active');
humanButtonEl.innerHTML = '✓Estás chateando con BNO Travel';
humanButtonEl.disabled = true;
quickPromptsEl.style.display = 'none';
queryEl.placeholder = 'Escribe un mensaje a BNO Travel …';
endSupportBtn.hidden = false;
}
function appendSupportIntro() {
clearInitialHint();
chatEl.insertAdjacentHTML('beforeend',
'
BNO Travel
Ahora estás chateando con BNO Travel
Escribe en qué necesitas ayuda. Cuando responda un miembro del equipo de atención, la respuesta aparecerá aquí.
Si sales de la página antes de que podamos responder, podremos ponernos en contacto contigo mediante el correo electrónico 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('Introduce tu nombre y correo electrónico para iniciar un chat con BNO Travel.');
chatEl.insertAdjacentHTML('beforeend',
'
' +
'
' +
'Chatea con un miembro de nuestro equipo' +
'
Indica tu nombre y correo electrónico. Así podremos ponernos en contacto contigo si sales del sitio antes de que podamos 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 directo con el equipo de atención de 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 = 'Introduce tu nombre.';
if (nameInput) nameInput.focus();
return;
}
if (!isValidEmail(email)) {
errorEl.textContent = 'Introduce un correo electrónico 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('Finalizando el chat …');
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('El chat ha finalizado. Puedes iniciar una nueva conversación.', 'success');
} catch (error) {
console.error('[BNO SUPPORT CLOSE]', error);
setStatus('No se ha podido finalizar el chat en este momento. Inténtalo de nuevo.', 'error');
} finally {
endSupportBtn.disabled = false;
}
}
async function sendTravelHelper(message) {
clearInitialHint();
quickPromptsEl.style.display = 'none';
appendUserMessage(message);
setStatus('El Asistente de Viaje BNO con IA está pensando …');
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, 'Asistente de Viaje BNO con IA');
else appendAssistantMessage(reply, 'Asistente de Viaje BNO con IA');
} else {
appendAssistantMessage(reply, 'Asistente de Viaje BNO con IA');
}
await renderTravelResponseExtras(data);
setStatus('');
} catch (error) {
console.error('[BNO WEB HELPER]', error);
appendAssistantMessage('Lo sentimos, el Asistente de Viaje BNO con IA no está disponible en este momento. Aún puedes elegir «Chatea con un miembro de nuestro equipo» para contactar con BNO Travel.', 'Asistente de Viaje BNO con IA');
setStatus('No se ha podido obtener una respuesta del Asistente de Viaje 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('Enviando 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('El mensaje se ha enviado 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('Debes indicar el nombre y el correo electrónico antes de enviar el mensaje.', 'error');
} else {
setStatus('No se ha podido enviar el mensaje. Inténtalo de nuevo.', '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 atención ha finalizado. Puedes iniciar una nueva conversación.', '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('Escribe primero un mensaje.');
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 directo con el equipo de atención de BNO Travel', 'success');
startSupportPolling();
}
})();