Precisa de Ajuda? Atendimento
0
Como comprar

Para comprar em nossa loja é muito fácil:

1) Basta navegar entre as diversas páginas de produtos ( categorias, novidades, ofertas, destaques) e clicar sobre o icone Comprar. Ou clique em Ver Detalhes para maiores informações sobre o produto. Após isso o produto será inserido em seu carrinho de compras


2) Continue navegando em nosso site até escolher todos os produtos que você deseja adquirir. Agora basta clicar no link a direita Finalizar que você estará concluindo sua compra.


3) Agora entre com o seu email e senha. Caso não for cadastrado, cadastre-se rapidamente clicando no link de Cadastro.


4) Feito isso, escolha a forma de envio do produto com nosso comercial.


5) Agora basta escolher a forma de pagamento.


6) Demais instruções serão enviadas via email para você após a finalização de sua venda.

`; } else { return '👋 Olá! Bem-vindo à Phoenix Hospitalar™! Estou aqui para te ajudar a escolher o equipamento hospitalar perfeito. O que você está procurando?'; } } function playNotificationSound() { try { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.value = 800; oscillator.type = 'sine'; gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.1); } catch (error) { console.error('[Phoenix Widget] Erro ao tocar som:', error); } } // FIX 2: linkify — processa markdown [texto](url), wa.me e URLs brutas function linkify(text) { const WA_MARKER = '\x00WA\x00'; const LINK_MARKER = '\x00LK\x00'; const SEP = '\x00'; // 1. Processar markdown [texto](url) ANTES de escapar HTML let result = text.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, function(_, label, url) { var cleanLabel = label.replace(/[\u{1F300}-\u{1FFFF}\u{2600}-\u{27FF}]/gu, '').trim(); if (url.indexOf('wa.me') !== -1) { return WA_MARKER + url + SEP + (cleanLabel || 'Abrir WhatsApp') + SEP; } return LINK_MARKER + url + SEP + (cleanLabel || url) + SEP; }); // 2. Remover emojis soltos antes de URLs brutas result = result.replace(/[\u{1F300}-\u{1FFFF}\u{2600}-\u{27FF}]\s*(?=https?:\/\/)/gu, ''); // 3. Processar URLs brutas restantes result = result.replace(/(https?:\/\/[^\s\x00<>"']+)/g, function(url) { if (url.indexOf('wa.me') !== -1) { return WA_MARKER + url + SEP + 'Abrir WhatsApp' + SEP; } return LINK_MARKER + url + SEP + url + SEP; }); // 4. Escapar HTML no texto puro (entre os marcadores) var parts = result.split(/(\x00(?:WA|LK)\x00[^\x00]+\x00[^\x00]+\x00)/g); var escaped = parts.map(function(part) { if (part.charAt(0) === '\x00') return part; return part.replace(/&/g, '&').replace(//g, '>'); }).join(''); // 5. Substituir marcadores por HTML final return escaped .replace(/\x00WA\x00([^\x00]+)\x00([^\x00]+)\x00/g, function(_, url, label) { return '' + label + ''; }) .replace(/\x00LK\x00([^\x00]+)\x00([^\x00]+)\x00/g, function(_, url, label) { var display = (label === url && url.length > 60) ? url.replace(/https?:\/\/[^/]+/, '').substring(0, 45) + '...' : label; return '' + display + ''; }); } const widgetHTML = `
`; document.body.insertAdjacentHTML('beforeend', widgetHTML); const button = document.getElementById('phoenix-chat-button'); const modal = document.getElementById('phoenix-chat-modal'); const closeBtn = document.getElementById('phoenix-chat-close'); const input = document.getElementById('phoenix-chat-input'); const sendBtn = document.getElementById('phoenix-chat-send'); const messagesContainer = document.getElementById('phoenix-chat-messages'); const flyingBird = document.getElementById('phoenix-flying-bird'); const hoveringBird = document.getElementById('phoenix-hovering'); const typingIndicator = document.getElementById('phoenix-typing-indicator'); state.sessionId = sessionStorage.getItem('phoenix_session_id'); if (!state.sessionId) { state.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; sessionStorage.setItem('phoenix_session_id', state.sessionId); } // FIX 1: startSession popula renderedMessageIds ANTES do polling iniciar async function startSession() { try { const response = await fetch(`${WIDGET_CONFIG.apiUrl}/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: state.sessionId, productUrl: window.location.href, isLocationPage: isLocationPage(), }), }); const data = await response.json(); if (!state.welcomeRendered) { state.welcomeRendered = true; const initialDiv = document.getElementById('phoenix-initial-message'); if (data.welcomeMessage) { // Sessão nova: renderizar boas-vindas e marcar ID sintético initialDiv.innerHTML = linkify(data.welcomeMessage); state.renderedMessageIds.add('welcome'); } else if (initialDiv) { // Sessão existente: esconder card de boas-vindas const card = initialDiv.closest('div[style]'); if (card) card.style.display = 'none'; } } // Marcar todos os IDs do histórico como já renderizados if (data.messages && data.messages.length > 0) { data.messages.forEach(function(m) { state.renderedMessageIds.add(m.id); if (m.id > (state.lastMessageId || 0)) state.lastMessageId = m.id; }); } } catch (error) { console.error('[Phoenix Widget] Error starting session:', error); } } // FIX 1: polling passa id para check-and-mark interno no addMessage function startPolling() { if (state.pollingTimer) clearInterval(state.pollingTimer); state.pollingTimer = setInterval(async () => { try { const url = `${WIDGET_CONFIG.apiUrl}/messages/${state.sessionId}${state.lastMessageId ? `?lastMessageId=${state.lastMessageId}` : ''}`; const response = await fetch(url); const data = await response.json(); if (data.success) { if (data.isTyping && !state.isTypingVisible) { typingIndicator.style.display = 'block'; state.isTypingVisible = true; } else if (!data.isTyping && state.isTypingVisible) { typingIndicator.style.display = 'none'; state.isTypingVisible = false; } if (data.messages && data.messages.length > 0) { data.messages.forEach(function(msg) { if (msg.sender !== 'user') { // FIX 1: passa msg.id para check-and-mark — evita duplicação com send addMessage(msg.content, msg.sender === 'vendor' ? 'vendor' : 'ai', msg.id); if (msg.id > (state.lastMessageId || 0)) state.lastMessageId = msg.id; playNotificationSound(); } }); if (state.isTypingVisible) { typingIndicator.style.display = 'none'; state.isTypingVisible = false; } } } } catch (error) { console.error('[Phoenix Widget] Error polling messages:', error); } }, WIDGET_CONFIG.pollingInterval); } function stopPolling() { if (state.pollingTimer) { clearInterval(state.pollingTimer); state.pollingTimer = null; } } async function openModal(autoOpened = false) { button.style.display = 'none'; flyingBird.style.display = 'block'; flyingBird.style.animation = 'flyUp 1.5s ease-out forwards'; setTimeout(async () => { modal.style.display = 'flex'; flyingBird.style.display = 'none'; hoveringBird.style.display = 'block'; input.focus(); // FIX 1: startSession PRIMEIRO, polling só inicia depois (sem race condition) await startSession(); startPolling(); }, 1000); markAsVisited(); } function closeModal() { modal.style.display = 'none'; hoveringBird.style.display = 'none'; button.style.display = 'flex'; stopPolling(); } if (!hasVisitedBefore()) { setTimeout(() => { openModal(true); }, WIDGET_CONFIG.autoOpenDelay); } button.addEventListener('click', openModal); closeBtn.addEventListener('click', closeModal); async function sendMessage() { const message = input.value.trim(); if (!message) return; addMessage(message, 'user'); input.value = ''; try { const response = await fetch(`${WIDGET_CONFIG.apiUrl}/send-message`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: state.sessionId, message, productUrl: window.location.href, isLocationPage: isLocationPage() }), }); const data = await response.json(); if (data.success && data.response) { // FIX 1: avançar lastMessageId para evitar re-exibição pelo polling if (data.aiMessageId && data.aiMessageId > (state.lastMessageId || 0)) { state.lastMessageId = data.aiMessageId; } if (data.welcomeMessageId && data.welcomeMessageId > (state.lastMessageId || 0)) { state.lastMessageId = data.welcomeMessageId; } // FIX 1: passa aiMessageId para check-and-mark — polling não re-exibe addMessage(data.response, 'ai', data.aiMessageId || null); if (data.welcomeMessageId) { state.renderedMessageIds.add(data.welcomeMessageId); } } else if (!data.success) { addMessage('Erro ao enviar mensagem. Tente novamente.', 'ai'); } } catch (error) { console.error('Erro ao enviar mensagem:', error); addMessage('Erro ao enviar mensagem. Tente novamente.', 'ai'); } } // FIX 1 + FIX 2 + FIX 3: addMessage com check-and-mark e linkify function addMessage(text, sender, id) { id = id || null; // FIX 1: check-and-mark — quem renderizar primeiro marca; segundo cai no return if (id && state.renderedMessageIds.has(id)) return; const isUser = sender === 'user'; const isVendor = sender === 'vendor'; const bgColor = isUser ? '#667eea' : (isVendor ? '#25D366' : '#f3f4f6'); const textColor = isUser || isVendor ? 'white' : '#1f2937'; const marginLeft = isUser ? '40px' : '0'; // FIX 2 + FIX 3: linkify processa markdown, URLs e remove emojis antes de URLs const formattedText = isUser ? text.replace(/&/g, '&').replace(//g, '>') : linkify(text); const messageHTML = `
${!isUser ? `
Phoenix
${isVendor ? 'Consultor Phoenix' : 'Phoenix Hospitalar'}
` : ''}
${formattedText}
`; messagesContainer.insertAdjacentHTML('beforeend', messageHTML); messagesContainer.scrollTop = messagesContainer.scrollHeight; // FIX 1: marcar APÓS render if (id) state.renderedMessageIds.add(id); } sendBtn.addEventListener('click', sendMessage); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); }); })();