/** * painel-conversa.js — Interface interativa de chat com o Carinha (IA Gemini / Qwen) * Suporte a Reconhecimento de Voz (Microfone), Síntese de Voz Robótica (TTS), * balões modernos, chips de sugestão e acionamento de expressões. */ (function() { 'use strict'; const el = id => document.getElementById(id); let sending = false; let signature = ''; let recognition = null; let isRecording = false; let lastSpokenMessage = null; // Configuração de síntese de voz robótica function falarTexto(texto) { if (!('speechSynthesis' in window) || !texto) return; window.speechSynthesis.cancel(); // Para falas anteriores const u = new SpeechSynthesisUtterance(texto); u.lang = 'pt-BR'; u.pitch = 1.35; // Voz aguda e robótica / simpática estilo robô u.rate = 1.05; // Ritmo levemente ágil // Tenta encontrar uma voz em português brasileiro const voices = window.speechSynthesis.getVoices(); const ptVoice = voices.find(v => v.lang.startsWith('pt')); if (ptVoice) u.voice = ptVoice; window.speechSynthesis.speak(u); } // Configuração do microfone (Speech Recognition) function setupSpeechRecognition() { const SpeechRec = window.SpeechRecognition || window.webkitSpeechRecognition; const micBtn = el('conversaMicBtn'); if (!SpeechRec || !micBtn) { if (micBtn) { micBtn.title = 'Reconhecimento de voz não suportado pelo navegador atual'; micBtn.style.opacity = '0.4'; } return; } recognition = new SpeechRec(); recognition.lang = 'pt-BR'; recognition.continuous = false; recognition.interimResults = false; recognition.onstart = () => { isRecording = true; micBtn.classList.add('recording'); micBtn.textContent = '⏹️'; if (window.RobotAudio) window.RobotAudio.click(); }; recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; const input = el('conversaTexto'); if (input && transcript) { input.value = transcript; input.focus(); if (window.RobotAudio) window.RobotAudio.bleep(); } }; recognition.onerror = (e) => { console.warn('Erro no microfone:', e.error); pararGravacao(); }; recognition.onend = () => { pararGravacao(); }; function pararGravacao() { isRecording = false; micBtn.classList.remove('recording'); micBtn.textContent = '🎤'; } micBtn.onclick = () => { if (isRecording) { recognition.stop(); } else { try { recognition.start(); } catch (err) { console.error('Falha ao iniciar reconhecimento:', err); } } }; } async function api(path, data) { const r = await fetch('/conversa/' + path, data === undefined ? { cache: 'no-store' } : { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await r.json(); if (!r.ok) throw Error(result.erro); return result; } async function refresh() { try { const s = await api('status'); if (el('conversaProvedor')) { el('conversaProvedor').value = s.provedor || 'local'; el('conversaProvedor').disabled = s.ocupado; } if (el('conversaEnviar')) { el('conversaEnviar').disabled = sending || s.ocupado; } // Indicador de "Pensando..." const typingEl = el('conversaTyping'); if (typingEl) { typingEl.style.display = s.ocupado ? 'flex' : 'none'; } // Texto de status do motor if (el('conversaEstado')) { el('conversaEstado').textContent = s.erro ? 'Falha: ' + s.erro : s.ocupado ? 'Pensando com ' + (s.modelo || 'IA') + '…' : s.ultima ? (s.modelo || 'IA') + ' · ' + s.ultima.segundos + 's' : 'Pronto para conversar.'; } const next = JSON.stringify([s.historico, s.lembrancas, s.ultima]); if (signature === next) return; signature = next; // Renderiza Histórico de Mensagens como Balões de Chat const hist = el('conversaHistorico'); if (hist) { hist.replaceChildren(); if (!s.historico || s.historico.length === 0) { const vazio = document.createElement('div'); vazio.style.cssText = 'text-align:center;color:var(--fraco);padding:40px 20px;font-size:14px'; vazio.innerHTML = `
Envie uma mensagem abaixo ou use os atalhos de sugestão.
`; hist.append(vazio); } else { for (const m of s.historico) { const row = document.createElement('div'); row.className = 'chat-row ' + (m.role === 'user' ? 'user' : 'assistant'); const avatar = document.createElement('div'); avatar.className = 'chat-avatar'; avatar.textContent = m.role === 'user' ? '👤' : '🤖'; const content = document.createElement('div'); content.className = 'chat-content'; const balloon = document.createElement('div'); balloon.className = 'chat-balloon'; balloon.textContent = m.content; content.append(balloon); // Se for mensagem do robô, adiciona botão de áudio e gesto if (m.role === 'assistant') { const meta = document.createElement('div'); meta.className = 'chat-meta'; const speakBtn = document.createElement('button'); speakBtn.className = 'chat-speak-btn'; speakBtn.type = 'button'; speakBtn.title = 'Ouvir resposta (Voz)'; speakBtn.innerHTML = '🔊 Falar'; speakBtn.onclick = () => { falarTexto(m.content); if (window.RobotAudio) window.RobotAudio.click(); }; meta.append(speakBtn); if (s.ultima && s.ultima.texto === m.content && s.ultima.gesto) { const gestoBadge = document.createElement('span'); gestoBadge.className = 'chat-gesto-badge'; gestoBadge.title = 'Clique para ver a carinha fazer este gesto'; gestoBadge.innerHTML = `✨${s.ultima.gesto}`; gestoBadge.onclick = async () => { try { if (window.RobotAudio) window.RobotAudio.tocarGesto(s.ultima.gesto); await fetch('/gesto/' + encodeURIComponent(s.ultima.gesto)); } catch (e) { console.error(e); } }; meta.append(gestoBadge); } content.append(meta); } row.append(avatar, content); hist.append(row); } hist.scrollTop = hist.scrollHeight; } // Fala automática da nova resposta da IA se a opção estiver ativada const autoVoz = el('conversaVozAuto')?.checked; if (autoVoz && s.ultima && s.ultima.texto && s.ultima.texto !== lastSpokenMessage) { lastSpokenMessage = s.ultima.texto; falarTexto(s.ultima.texto); if (s.ultima.gesto && window.RobotAudio) { window.RobotAudio.tocarGesto(s.ultima.gesto); } } } // Renderiza Lista de Lembranças / Memória const memorias = el('conversaMemorias'); if (memorias) { memorias.replaceChildren(); for (const m of s.lembrancas || []) { const row = document.createElement('div'); row.className = 'memory-card'; const label = document.createElement('span'); label.style.cssText = 'color:var(--texto);font-size:13.5px;word-break:break-word'; label.textContent = m.texto; const button = document.createElement('button'); button.type = 'button'; button.className = 'danger'; button.style.cssText = 'padding:4px 10px;font-size:12px;flex-shrink:0'; button.textContent = 'Apagar'; button.onclick = () => action('apagar', { id: m.id }); row.append(label, button); memorias.append(row); } if (!s.lembrancas || !s.lembrancas.length) { memorias.innerHTML = 'Nenhuma lembrança salva no banco SQLite.'; } } } catch (e) { if (el('conversaEstado')) el('conversaEstado').textContent = e.message; } } async function action(path, data) { try { await api(path, data); await refresh(); return true; } catch (e) { if (el('conversaEstado')) el('conversaEstado').textContent = e.message; return false; } } // Troca de Provedor (Gemini / Qwen) if (el('conversaProvedor')) { el('conversaProvedor').onchange = () => action('config', { provedor: el('conversaProvedor').value }); } // Envio de Mensagem if (el('conversaForm')) { el('conversaForm').onsubmit = async (e) => { e.preventDefault(); const input = el('conversaTexto'); const val = input.value.trim(); if (!val) return; sending = true; if (el('conversaEnviar')) el('conversaEnviar').disabled = true; if (window.RobotAudio) window.RobotAudio.click(); try { if (await action('enviar', { texto: val })) { input.value = ''; } } finally { sending = false; await refresh(); } }; } // Chips de Sugestão de Diálogo document.querySelectorAll('.suggestion-chip').forEach(chip => { chip.onclick = async () => { const msg = chip.getAttribute('data-msg'); const input = el('conversaTexto'); if (input && msg) { input.value = msg; if (el('conversaForm')) { el('conversaForm').requestSubmit(); } } }; }); // Salvar Lembrança if (el('memoriaForm')) { el('memoriaForm').onsubmit = async (e) => { e.preventDefault(); const input = el('memoriaTexto'); if (await action('lembrar', { texto: input.value })) { input.value = ''; if (window.RobotAudio) window.RobotAudio.bleep(); } }; } // Limpar Histórico if (el('conversaLimpar')) { el('conversaLimpar').onclick = () => { if (confirm('Apagar todo o histórico de conversas e lembranças do Carinha?')) { action('limpar', {}); } }; } setupSpeechRecognition(); refresh(); setInterval(refresh, 1800); })();