const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; let recognition = null; let isListening = false; let sessionId = "fer-" + Date.now(); let token = localStorage.getItem("jarvis_token") || "jarvis-token-20261008"; const API = "/api"; const micBtn = document.getElementById('mic-btn'); const messageInput = document.getElementById('message-input'); const sendBtn = document.getElementById('send-btn'); const messagesContainer = document.getElementById('messages'); const speechWarning = document.getElementById('speech-support-warning'); function checkSpeechSupport() { if (!SpeechRecognition) { speechWarning.classList.remove('hidden'); micBtn.disabled = true; micBtn.style.opacity = '0.5'; micBtn.style.cursor = 'not-allowed'; return false; } return true; } function initRecognition() { if (!SpeechRecognition) return null; const rec = new SpeechRecognition(); rec.continuous = true; rec.interimResults = true; rec.lang = 'es-AR'; return rec; } let voiceEnabled = localStorage.getItem("jarvis_voice") !== "off"; let voiceBtn = document.getElementById('voice-btn'); function hablar(texto) { if (!voiceEnabled) return; window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(texto); utterance.lang = 'es-AR'; utterance.rate = 1.0; utterance.pitch = 1.0; const voces = window.speechSynthesis.getVoices(); const vozEs = voces.find(v => v.lang.startsWith('es')); if (vozEs) utterance.voice = vozEs; window.speechSynthesis.speak(utterance); } function toggleVoice() { voiceEnabled = !voiceEnabled; localStorage.setItem("jarvis_voice", voiceEnabled ? "on" : "off"); voiceBtn.textContent = voiceEnabled ? '🔊' : '🔇'; voiceBtn.classList.toggle('muted', !voiceEnabled); } function addMessage(role, text, isInterim = false) { const div = document.createElement('div'); div.className = `message ${role} ${isInterim ? 'interim' : ''}`; div.textContent = text; messagesContainer.appendChild(div); messagesContainer.scrollTop = messagesContainer.scrollHeight; return div; } function updateLastMessage(text, isInterim = false) { const msgs = messagesContainer.querySelectorAll('.message.assistant.interim, .message.user.interim'); if (msgs.length > 0) { msgs[msgs.length - 1].textContent = text; msgs[msgs.length - 1].classList.toggle('interim', isInterim); } } async function sendMessage(text) { if (!text.trim()) return; addMessage('user', text); messageInput.value = ''; autoResize(messageInput); const interimDiv = addMessage('assistant', '...', true); try { const res = await fetch(`${API}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Jarvis-Token': token }, body: JSON.stringify({ message: text, session_id: sessionId }) }); const data = await res.json(); interimDiv.remove(); if (data.reply) { addMessage('assistant', data.reply); hablar(data.reply); } else if (data.error) { addMessage('assistant', `Error: ${data.error}`); } } catch (e) { interimDiv.remove(); addMessage('assistant', 'No pude conectar con Jarvis.'); } } function autoResize(textarea) { textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; } function startListening() { if (!checkSpeechSupport() || isListening) return; recognition = initRecognition(); if (!recognition) return; let finalTranscript = ''; let interimDiv = null; recognition.onresult = (event) => { let interimTranscript = ''; for (let i = event.resultIndex; i < event.results.length; i++) { const transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript + ' '; } else { interimTranscript += transcript; } } if (interimTranscript) { if (!interimDiv) { interimDiv = addMessage('user', interimTranscript, true); } else { interimDiv.textContent = interimTranscript; } } if (finalTranscript) { if (interimDiv) interimDiv.remove(); sendMessage(finalTranscript.trim()); finalTranscript = ''; } }; recognition.onerror = (e) => { console.warn('Speech error:', e.error); if (e.error !== 'no-speech' && e.error !== 'aborted') { stopListening(); } }; recognition.onend = () => { if (isListening) { // Reiniciar para mantener abierto (workaround navegador) try { recognition.start(); } catch(e) { stopListening(); } } }; isListening = true; micBtn.classList.add('listening'); micBtn.setAttribute('aria-label', 'Dejar de escuchar'); recognition.start(); } function stopListening() { if (!isListening) return; isListening = false; if (recognition) { try { recognition.stop(); } catch(e) {} } micBtn.classList.remove('listening'); micBtn.setAttribute('aria-label', 'Dictar mensaje'); } function toggleListening() { if (isListening) stopListening(); else startListening(); } function checkAvisos() { fetch(`${API}/estado?session_id=${sessionId}`, { headers: { 'X-Jarvis-Token': token } }).then(r => r.json()).then(data => { if (data.avisos && data.avisos.length > 0) { data.avisos.forEach(a => addMessage('assistant', a.content)); } }).catch(() => {}); } // Eventos sendBtn.addEventListener('click', () => sendMessage(messageInput.value)); messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(messageInput.value); } }); messageInput.addEventListener('input', () => autoResize(messageInput)); micBtn.addEventListener('click', toggleListening); // Polling avisos cada 10s setInterval(checkAvisos, 10000); // Init if (voiceBtn) { voiceBtn.textContent = voiceEnabled ? '🔊' : '🔇'; voiceBtn.addEventListener('click', toggleVoice); } checkSpeechSupport(); console.log('Jarvis PWA listo');