feat: initial commit - Carinha Control Panel v1.0 com UI glassmorphism, watch-bar, emojis, braco robotico e conversa IA
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* 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);
|
||||
})();
|
||||
Reference in New Issue
Block a user