window.DIRECTWISE_SITE_ID='a1111111-1111-1111-1111-111111111111'; window.DIRECTWISE_API_URL='https://directwise.forclus.net'; window.DIRECTWISE_VOICE_ENABLED=true; (function () { // Get site_id and API URL injected by backend const SITE_ID = window.DIRECTWISE_SITE_ID; const API_BASE = window.DIRECTWISE_API_URL || ''; if (!SITE_ID) { console.error('DirectWise Error: site_id not provided'); return; } if (!API_BASE) { console.error('DirectWise Error: API base URL not provided'); return; } // Session ID management function getSessionId() { return localStorage.getItem('directwise_session_id'); } function setSessionId(sid) { localStorage.setItem('directwise_session_id', sid); } // Responsive widget size function getWidgetDimensions() { if (window.innerWidth < 700) { return { width: Math.min(window.innerWidth * 0.90, 390), height: Math.min(window.innerHeight * 0.90, 600) }; } else { return { width: 430, height: 540 }; } } // Chat UI const container = document.createElement('div'); container.id = 'chat-container'; container.setAttribute('data-dw-widget', '1'); container.style.cssText = ` display:none; position:fixed; bottom:90px; right:20px; width:0; height:0; background:#fff; border-radius:14px; box-shadow:0 8px 20px rgba(0,0,0,0.25); flex-direction:column; overflow:hidden; z-index:9999; opacity:0; `; container.innerHTML = `
`; document.body.appendChild(container); const toggle = document.createElement('div'); toggle.id = 'chat-toggle'; toggle.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 64px; height: 64px; border-radius: 50%; background: radial-gradient(circle at 30% 30%, #ffe066, #ffcc00); border: none; box-shadow: 0 6px 20px rgba(255, 204, 0, 0.6); font-size: 28px; line-height: 64px; text-align: center; cursor: pointer; z-index: 10000; transition: transform 0.3s ease, box-shadow 0.3s ease; animation: pulse 2s infinite; `; toggle.textContent = '💡'; document.body.appendChild(toggle); // Blindar estilos ante modo oscuro o resets globales const style = document.createElement('style'); style.textContent = ` #chat-container, #chat-container * { font-family: 'Segoe UI', 'Arial', sans-serif !important; box-shadow: none !important; text-shadow: none !important; font-size: 15px !important; line-height: 1.6 !important; letter-spacing: 0 !important; border: none !important; } #chat-container { background: #fff !important; color: #222 !important; box-shadow: 0 8px 20px rgba(0,0,0,0.25) !important; } #chat-messages { background: none !important; color: #222 !important; position: relative !important; } .message.user { background: #dcf8c6 !important; color: #222 !important; } .message.bot, .message.bot * { background: #f1f0f0 !important; color: #222 !important; } #chat-send-btn { background: linear-gradient(to right, #ffd633, #ffb700) !important; color: #333 !important; box-shadow: 0 4px 10px rgba(255, 204, 0, 0.4) !important; } #chat-send-btn:hover { background: linear-gradient(to right, #ffe066, #ffc400) !important; color: #222 !important; } #chat-send-btn:active { color: #222 !important; } .thinking-anim { font-style: italic !important; color: #999 !important; letter-spacing: 1px !important; } .dw-highlight { background: #ffe870 !important; color: #222 !important; border-radius: 4px !important; font-weight: inherit !important; padding: 2px 2px !important; box-shadow: 0 0 2px #ffe870, 0 0 0 #fff0 !important; transition: background 0.2s !important; z-index: 99999 !important; display: inline !important; } #chat-container a, #chat-container .enriched-link { color: #1a66cc !important; text-decoration: underline !important; font-weight: 600 !important; cursor: pointer !important; background: none !important; } #chat-container a:visited, #chat-container .enriched-link:visited { color: #744fc7 !important; } #chat-container a:hover, #chat-container .enriched-link:hover { color: #ee8800 !important; text-decoration: underline !important; } @media (max-width: 700px) { #chat-container { right: 1vw !important; bottom: 60px !important; border-radius: 12px !important; width: 90vw !important; max-width: 90vw !important; height: 90vh !important; max-height: 90vh !important; min-width: 0 !important; min-height: 0 !important; padding: 0 !important; } #chat-toggle { right: 2vw !important; bottom: 8px !important; width: 54px !important; height: 54px !important; font-size: 22px !important; } } #chat-fade-top { pointer-events: none !important; position: absolute !important; left: 0 !important; top: 0 !important; width: 100% !important; height: 36px !important; z-index: 2 !important; background: linear-gradient(to bottom, rgba(255,255,255,0.98) 78%, rgba(255,255,255,0)) !important; opacity: 0; /* Empieza oculto */ transition: opacity 0.25s !important; display: none !important; } #chat-fade-bottom { pointer-events: none !important; position: absolute !important; left: 0 !important; bottom: 0 !important; width: 100% !important; height: 30px !important; z-index: 2 !important; background: linear-gradient(to top, rgba(255,255,255,0.95) 70%, rgba(255,255,255,0) 100%) !important; opacity: 0; /* Empieza oculto */ transition: opacity 0.25s !important; display: none !important; } @keyframes pulse { 0% { transform: scale(1); box-shadow: 0 0 0 rgba(255, 204, 0, 0.6); } 50% { transform: scale(1.05); box-shadow: 0 0 10px rgba(255, 204, 0, 0.8); } 100% { transform: scale(1); box-shadow: 0 0 0 rgba(255, 204, 0, 0.6); } } `; document.head.appendChild(style); // UI logic const form = container.querySelector('#chat-form'); const input = container.querySelector('#chat-input'); const messages = container.querySelector('#chat-messages'); const fadeTop = container.querySelector('#chat-fade-top'); const fadeBottom = container.querySelector('#chat-fade-bottom'); let isOpen = false; function animateContainer(open) { const { width: targetWidth, height: targetHeight } = getWidgetDimensions(); let width = open ? 0 : targetWidth; let height = open ? 0 : targetHeight; let opacity = open ? 0 : 1; let step = 16; container.style.display = 'flex'; const interval = setInterval(function () { if (open) { if (width < targetWidth) width += step; if (height < targetHeight) height += step * 1.33; if (opacity < 1) opacity += 0.05; } else { if (width > 0) width -= step; if (height > 0) height -= step * 1.33; if (opacity > 0) opacity -= 0.05; } container.style.width = Math.min(width, targetWidth) + 'px'; container.style.height = Math.min(height, targetHeight) + 'px'; container.style.opacity = opacity; if ((open && width >= targetWidth && height >= targetHeight) || (!open && width <= 0 && height <= 0)) { clearInterval(interval); if (!open) container.style.display = 'none'; } }, 10); } toggle.addEventListener('click', () => { isOpen = !isOpen; animateContainer(isOpen); }); input.addEventListener('input', () => { input.style.height = 'auto'; input.style.height = (input.scrollHeight) + 'px'; }); function scrollToBottom() { messages.scrollTop = messages.scrollHeight; updateFade(); } // --- FADES arriba y abajo --- function updateFade() { // Fade superior: solo aparece si hay scroll arriba if (messages.scrollTop > 2) { fadeTop.style.display = ''; fadeTop.style.opacity = '0.96'; } else { fadeTop.style.opacity = '0'; setTimeout(() => { if (messages.scrollTop <= 2) fadeTop.style.display = 'none'; }, 250); } // Fade inferior: solo si no estamos al final del scroll if (messages.scrollHeight - messages.clientHeight - messages.scrollTop > 3) { fadeBottom.style.display = ''; fadeBottom.style.opacity = '0.93'; } else { fadeBottom.style.opacity = '0'; setTimeout(() => { if (messages.scrollHeight - messages.clientHeight - messages.scrollTop <= 3) fadeBottom.style.display = 'none'; }, 250); } } messages.addEventListener('scroll', updateFade); // --- Animación tipo máquina de escribir + highlights --- function typeBotMessage(text) { // Parser de bloques (normal y highlight) const highlightRegex = /\[([^\]]+)\]\(#highlight:([^:]+)::([^)]+)\)/g; let result, lastIndex = 0, blocks = []; while ((result = highlightRegex.exec(text)) !== null) { if (result.index > lastIndex) { blocks.push({ type: 'normal', text: text.substring(lastIndex, result.index) }); } blocks.push({ type: 'highlight', visible: result[1], url: result[2], fragment: result[3] }); lastIndex = highlightRegex.lastIndex; } if (lastIndex < text.length) { blocks.push({ type: 'normal', text: text.substring(lastIndex) }); } // Crear el div mensaje y limpiar const div = document.createElement('div'); div.className = 'message bot'; div.style.cssText = ` margin-bottom: 8px; padding: 8px 12px; border-radius: 12px; max-width: 70%; background: #f1f0f0; align-self: flex-start; opacity: 1; `; messages.appendChild(div); // Crear spans para cada bloque const spans = []; blocks.forEach(block => { let span; if (block.type === 'normal') { span = document.createElement('span'); span.className = 'dw-text'; span.textContent = ''; } else if (block.type === 'highlight') { span = document.createElement('a'); span.className = 'enriched-link'; span.href = 'javascript:void(0)'; span.dataset.url = block.url.replace(/"/g, """); span.dataset.fragment = block.fragment.replace(/"/g, """); span.innerHTML = ''; } div.appendChild(span); spans.push({ span, block }); }); scrollToBottom(); function animateBlock(iBlock, iChar) { if (iBlock >= blocks.length) return; const { span, block } = spans[iBlock]; if (block.type === 'normal') { if (iChar < block.text.length) { span.textContent += block.text[iChar]; scrollToBottom(); setTimeout(() => animateBlock(iBlock, iChar + 1), 18); } else { animateBlock(iBlock + 1, 0); } } else if (block.type === 'highlight') { // Escribir el fragmento de golpe span.innerHTML = block.visible.replace(//g, ">"); scrollToBottom(); setTimeout(() => animateBlock(iBlock + 1, 0), 140); } } animateBlock(0, 0); } // --- Thinking Animation --- let thinkingInterval = null; function showThinkingAnimated() { removeThinking(); const div = document.createElement('div'); div.className = 'message bot thinking-anim'; div.id = 'thinking-message'; div.style.cssText = ` margin-bottom: 8px; padding: 8px 12px; border-radius: 12px; max-width: 70%; background: #f1f0f0; align-self: flex-start; opacity: 1; font-style: italic; color: #999; letter-spacing: 1px; `; div.textContent = "Thinking"; messages.appendChild(div); scrollToBottom(); let dots = 0; thinkingInterval = setInterval(() => { dots = (dots + 1) % 4; div.textContent = "Thinking" + ".".repeat(dots); }, 400); } function removeThinking() { if (thinkingInterval) clearInterval(thinkingInterval); thinkingInterval = null; const div = document.getElementById('thinking-message'); if (div) div.remove(); } function enrichReplyText(text) { return text.replace(/\[([^\]]+)\]\(#highlight:([^:]+)::([^)]+)\)/g, function (match, visible, url, fragment) { const safeVisible = visible.replace(//g, ">"); const safeUrl = url.replace(/"/g, """); const safeFragment = fragment.replace(/"/g, """); return `${safeVisible}`; }); } // Highlight fragment (solo texto) function highlightFragment(fragment) { let fragNorm = fragment.trim(); let candidates = [fragNorm]; let parts = fragNorm.split(' '); if (parts.length > 8) candidates.push(parts.slice(0, 8).join(' ')); if (parts.length > 5) candidates.push(parts.slice(0, 5).join(' ')); if (parts.length > 2) candidates.push(parts[0]); const els = Array.from(document.querySelectorAll('body p, body li, body h1, body h2, body h3, body h4, body h5, body span, body div')); for (const el of els) { let skip = false, n = el; while (n) { if (n.hasAttribute && n.hasAttribute('data-dw-widget')) { skip = true; break; } n = n.parentElement; } if (skip) continue; for (const node of el.childNodes) { if (node.nodeType === 3) { // Text node let text = node.nodeValue; for (const c of candidates) { const idx = text.toLowerCase().indexOf(c.toLowerCase()); if (idx !== -1) { const before = text.slice(0, idx); const match = text.slice(idx, idx + c.length); const after = text.slice(idx + c.length); const beforeNode = document.createTextNode(before); const highlightNode = document.createElement('span'); highlightNode.className = "dw-highlight"; highlightNode.textContent = match; const afterNode = document.createTextNode(after); const originalNode = node; el.insertBefore(beforeNode, node); el.insertBefore(highlightNode, node); el.insertBefore(afterNode, node); el.removeChild(node); el.scrollIntoView({ behavior: "smooth", block: "center" }); setTimeout(() => { el.replaceChild(originalNode, beforeNode); el.removeChild(highlightNode); el.removeChild(afterNode); }, 3000); return; } } } } } } messages.addEventListener('click', function (e) { const link = e.target.closest('.enriched-link'); if (link) { const url = link.getAttribute('data-url'); const fragment = link.getAttribute('data-fragment'); if (window.location.pathname !== url) { sessionStorage.setItem('dw-highlight', JSON.stringify({ url, fragment })); window.location.href = url; } else { highlightFragment(fragment); } e.preventDefault(); } }); window.addEventListener('DOMContentLoaded', () => { const data = sessionStorage.getItem('dw-highlight'); if (data) { try { const { url, fragment } = JSON.parse(data); if (window.location.pathname === url && fragment) { isOpen = true; animateContainer(isOpen); setTimeout(() => highlightFragment(fragment), 350); } } catch (e) { } sessionStorage.removeItem('dw-highlight'); } }); function loadHistory() { const sid = getSessionId(); if (!sid) return; fetch(API_BASE + '/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ site_id: SITE_ID, session_id: sid }) }) .then(res => res.json()) .then(data => { if (data.history && data.history.length > 0) { messages.innerHTML = ''; data.history.forEach(msg => { if (msg.role === 'user') addMessage(msg.content, 'user'); else if (msg.role === 'assistant') addMessageHtml(enrichReplyText(msg.content), 'bot'); }); } updateFade(); }) .catch(err => { console.error('DirectWise Error loading history:', err); }); } loadHistory(); function fetchWithTimeout(url, options, timeout = 18000) { return Promise.race([ fetch(url, options), new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout exceeded")), timeout)) ]); } function addMessage(text, type) { const div = document.createElement('div'); div.className = 'message ' + type; div.style.cssText = ` margin-bottom: 8px; padding: 8px 12px; border-radius: 12px; max-width: 70%; opacity: 0; transition: opacity 0.4s ease; background: ${type === 'user' ? '#dcf8c6' : '#f1f0f0'}; align-self: ${type === 'user' ? 'flex-end' : 'flex-start'}; `; div.textContent = text; messages.appendChild(div); setTimeout(() => div.style.opacity = 1, 50); scrollToBottom(); } function addMessageHtml(html, type) { const div = document.createElement('div'); div.className = 'message ' + type; div.style.cssText = ` margin-bottom: 8px; padding: 8px 12px; border-radius: 12px; max-width: 70%; opacity: 0; transition: opacity 0.4s ease; background: ${type === 'user' ? '#dcf8c6' : '#f1f0f0'}; align-self: ${type === 'user' ? 'flex-end' : 'flex-start'}; `; div.innerHTML = html; messages.appendChild(div); setTimeout(() => div.style.opacity = 1, 50); scrollToBottom(); } form.addEventListener('submit', async (e) => { e.preventDefault(); let text = input.value.trim(); const MAX_LEN = 350; if (text.length > MAX_LEN) { addMessageHtml('Tu mensaje es demasiado largo (máx. '+MAX_LEN+' caracteres).', 'bot'); return; } addMessage(text, 'user'); input.value = ''; input.style.height = 'auto'; showThinkingAnimated(); let payload = { site_id: SITE_ID, message: text }; const sid = getSessionId(); if (sid) payload.session_id = sid; try { const res = await fetchWithTimeout(API_BASE + '/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if (data.session_id) setSessionId(data.session_id); removeThinking(); if (data.history && data.history.length > 0) { messages.innerHTML = ''; data.history.forEach(msg => { if (msg.role === 'user') addMessage(msg.content, 'user'); else if (msg.role === 'assistant') addMessageHtml(enrichReplyText(msg.content), 'bot'); }); } if (data.reply) typeBotMessage(data.reply); updateFade(); } catch (err) { removeThinking(); if (err.message && err.message.toLowerCase().includes('timeout')) { typeBotMessage('Sorry, the assistant took too long to respond. Please try again.'); } else { typeBotMessage('An error occurred. Please try again later.'); } console.error('DirectWise Error:', err); } }); // ==== Config inicial ==== const VOICE_ENABLED = window.DIRECTWISE_VOICE_ENABLED === true; let ws = null, recording = false, mediaRecorder = null, currentSessionId = null; let micBtn, silenceTimeout, audioStream, audioCtx, analyser, dataArray, listening = false; const SILENCE_THRESHOLD = 0.02; // sensibilidad (sube si es muy sensible) const SILENCE_MS = 1200; // milisegundos de silencio antes de enviar // ====== BOTÓN MICRO ====== if (VOICE_ENABLED) { micBtn = document.createElement('button'); micBtn.type = 'button'; micBtn.id = 'voice-btn'; micBtn.title = 'Pulsa para hablar'; micBtn.innerHTML = '🎤'; micBtn.style.cssText = ` margin-left: 5px; background: #fff8; border: 1px solid #eee; border-radius: 18px; font-size: 21px; width: 38px; height: 38px; cursor: pointer; transition: background 0.2s; `; micBtn.addEventListener('mousedown', toggleVoice); micBtn.addEventListener('touchstart', toggleVoice); form.appendChild(micBtn); } // ====== INICIAR/PARAR ESCUCHA ====== function toggleVoice(e) { if (!listening) { startVoice(); micBtn.style.background = '#ffe870'; micBtn.style.color = '#222'; listening = true; } else { stopAllVoice(); micBtn.style.background = '#fff8'; micBtn.style.color = ''; listening = false; } if (e) e.preventDefault(); } // ====== Detección de silencio y grabación ====== function startVoice() { navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { audioStream = stream; audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const source = audioCtx.createMediaStreamSource(stream); analyser = audioCtx.createAnalyser(); source.connect(analyser); dataArray = new Float32Array(analyser.fftSize); function checkSilence() { analyser.getFloatTimeDomainData(dataArray); // Cálculo de volumen RMS let rms = Math.sqrt(dataArray.reduce((sum, v) => sum + v * v, 0) / dataArray.length); if (rms < SILENCE_THRESHOLD) { // Silencio detectado if (!silenceTimeout) { silenceTimeout = setTimeout(() => { if (recording) { mediaRecorder.stop(); recording = false; } }, SILENCE_MS); } } else { // Voz detectada if (!recording) startMediaRecorder(); if (silenceTimeout) { clearTimeout(silenceTimeout); silenceTimeout = null; } } if (listening) requestAnimationFrame(checkSilence); } // Arranca el bucle de chequeo requestAnimationFrame(checkSilence); }); } function startMediaRecorder() { mediaRecorder = new MediaRecorder(audioStream, { mimeType: 'audio/webm' }); let chunks = []; mediaRecorder.ondataavailable = event => chunks.push(event.data); mediaRecorder.onstop = () => { const blob = new Blob(chunks, { type: 'audio/webm' }); sendVoice(blob); chunks = []; // Si sigue listening, el loop de checkSilence seguirá esperando nuevo sonido }; mediaRecorder.start(); recording = true; } // ====== Parar todo al pulsar de nuevo ====== function stopAllVoice() { listening = false; if (mediaRecorder && recording) { mediaRecorder.stop(); recording = false; } if (audioStream) { audioStream.getTracks().forEach(track => track.stop()); audioStream = null; } if (audioCtx) { audioCtx.close(); audioCtx = null; } if (silenceTimeout) { clearTimeout(silenceTimeout); silenceTimeout = null; } } // ====== Enviar audio por WebSocket ====== function sendVoice(blob) { const reader = new FileReader(); reader.onload = function () { const audioBase64 = reader.result.split(',')[1]; ensureWs(function () { ws.send(JSON.stringify({ type: "audio", audio: audioBase64 })); if (typeof showThinkingAnimated === "function") showThinkingAnimated(); }); }; reader.readAsDataURL(blob); } // ====== WEBSOCKET ====== function ensureWs(cb) { // Si ya existe y está abierta, OK if (ws && ws.readyState === 1) { cb(); return; } // Si no, crea y conecta: ws = new window.WebSocket(window.DIRECTWISE_API_URL.replace(/^http/, 'ws') + '/api/voice'); ws.onopen = function () { ws.send(JSON.stringify({ site_id: SITE_ID, session_id: getSessionId ? (getSessionId() || undefined) : undefined // Reusa sesión })); cb && cb(); }; ws.onmessage = function (evt) { const msg = JSON.parse(evt.data); if (msg.type === "init" && msg.session_id) { if (typeof setSessionId === "function") setSessionId(msg.session_id); currentSessionId = msg.session_id; } else if (msg.type === "reply") { if (typeof removeThinking === "function") removeThinking(); if (msg.transcript && typeof typeBotMessage === "function") typeBotMessage(msg.transcript); if (msg.audio) playBase64Audio(msg.audio); } else if (msg.error) { if (typeof removeThinking === "function") removeThinking(); if (typeof typeBotMessage === "function") typeBotMessage(msg.error); } }; ws.onclose = function () { ws = null; }; ws.onerror = function (e) { ws = null; }; } // ====== Reproducir audio base64 (webm) ====== function playBase64Audio(base64) { const audio = new Audio('data:audio/webm;base64,' + base64); audio.play(); } })();