Files
carinha-control-panel/painel-conversa.js
T

329 lines
11 KiB
JavaScript

/**
* 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 = `
<div style="font-size:36px;margin-bottom:10px">🤖</div>
<strong>O Carinha está pronto para conversar!</strong>
<p style="margin-top:6px;font-size:13px">Envie uma mensagem abaixo ou use os atalhos de sugestão.</p>
`;
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 = `<span>✨</span><span>${s.ultima.gesto}</span>`;
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 = '<span style="color:var(--fraco);font-size:13px;padding:6px 0">Nenhuma lembrança salva no banco SQLite.</span>';
}
}
} 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);
})();