sintonIA / index.html
sbenfenatti's picture
Upload 6 files
d8621a7 verified
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SintonIA</title>
<!-- Tailwind CSS via CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Google Fonts: Inter -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(-45deg, #A8D5BA, #F4A261, #84A98C, #E76F51);
background-size: 400% 400%;
animation: gradientBG 15s ease infinite;
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.glass-effect {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.recording-pulse {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(231, 111, 81, 0.7); }
70% { box-shadow: 0 0 0 20px rgba(231, 111, 81, 0); }
100% { box-shadow: 0 0 0 0 rgba(231, 111, 81, 0); }
}
</style>
</head>
<body class="text-gray-800">
<!-- Container Principal -->
<div class="flex items-center justify-center min-h-screen w-full p-4">
<!-- ===== TELA DE LOGIN ===== -->
<div id="login-page" class="w-full max-w-sm p-8 space-y-6 glass-effect rounded-2xl shadow-lg">
<div class="text-center">
<img src="https://i.postimg.cc/GpTXqLxn/temp-Image3-DT7y-Z.avif" alt="Logo SintonIA" class="w-28 h-28 mx-auto mb-4 rounded-full object-cover">
</div>
<div class="space-y-4">
<div>
<label for="password-input" class="sr-only">Senha</label>
<input id="password-input" type="password" required class="w-full px-4 py-3 bg-white/50 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" placeholder="Senha">
</div>
<button id="login-button" class="w-full px-4 py-3 font-semibold text-white bg-teal-600 rounded-lg hover:bg-teal-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 transition-colors">
Entrar
</button>
<p id="error-message" class="text-sm text-center text-red-600 h-4"></p>
</div>
</div>
<!-- ===== TELA DE CHAT ===== -->
<div id="chat-page" class="hidden w-full max-w-2xl h-[90vh] flex flex-col glass-effect rounded-2xl shadow-2xl">
<header class="p-4 border-b border-white/30 flex-shrink-0 flex items-center justify-center">
<img src="https://i.postimg.cc/GpTXqLxn/temp-Image3-DT7y-Z.avif" alt="Logo SintonIA" class="w-14 h-14 rounded-full object-cover">
</header>
<main id="chat-history" class="flex-grow p-6 space-y-6 overflow-y-auto">
<div class="flex justify-start">
<div class="px-4 py-3 bg-gray-200 rounded-xl max-w-lg">
<p class="text-sm">Olá! Sou seu assistente de saúde bucal. Clique no botão abaixo para começar a gravar.</p>
</div>
</div>
</main>
<footer class="p-6 flex-shrink-0 flex flex-col items-center justify-center space-y-3">
<button id="record-button" class="w-24 h-24 bg-teal-500 text-white rounded-full flex items-center justify-center shadow-lg transform hover:scale-105 transition-transform focus:outline-none focus:ring-4 focus:ring-teal-300">
<i id="record-icon" data-lucide="mic" class="w-10 h-10"></i>
</button>
<p id="status-text" class="text-sm text-gray-700 h-5">Clique para falar</p>
</footer>
</div>
</div>
<!-- Créditos -->
<footer class="absolute bottom-4 left-1/2 -translate-x-1/2 text-center">
<p class="text-xs text-white/80">Desenvolvido por Sérgio H. Benfenatti Botelho - DDS, LLM trainer - 🇧🇷</p>
</footer>
<script>
// --- Seleção de Elementos do DOM ---
const loginPage = document.getElementById('login-page');
const chatPage = document.getElementById('chat-page');
const passwordInput = document.getElementById('password-input');
const loginButton = document.getElementById('login-button');
const errorMessage = document.getElementById('error-message');
const recordButton = document.getElementById('record-button');
const recordIcon = document.getElementById('record-icon');
const statusText = document.getElementById('status-text');
const chatHistory = document.getElementById('chat-history');
// --- Variáveis de Estado ---
let mediaRecorder;
let audioChunks = [];
let isRecording = false;
let audioPlayer = new Audio();
let isAudioContextUnlocked = false; // --- CORREÇÃO: Variável de controle re-adicionada
// --- Inicialização ---
lucide.createIcons();
// --- CORREÇÃO: Lógica para "destravar" o áudio re-adicionada ---
function primeAudioContext() {
if (isAudioContextUnlocked) return;
// Toca um som silencioso para "acordar" o player de áudio do navegador.
audioPlayer.src = "data:audio/mpeg;base64,SUQzBAAAAAABEVRYWFgAAAAtAAADY29tbWVudABCaXRyYXRlIHN1cHBseSBieSBiaXRyYXRlLmNvbQAAAABUY29uAAAAAABQaG9uZQAAAAA=";
const playPromise = audioPlayer.play();
if (playPromise !== undefined) {
playPromise.then(() => {
audioPlayer.pause();
isAudioContextUnlocked = true;
console.log("Contexto de áudio desbloqueado e preparado.");
}).catch(error => {
console.error("Falha ao preparar o contexto de áudio:", error);
});
}
}
// --- Lógica de Autenticação ---
loginButton.addEventListener('click', handleLogin);
passwordInput.addEventListener('keyup', (event) => {
if (event.key === 'Enter') handleLogin();
});
async function handleLogin() {
// --- CORREÇÃO: Prepara o áudio no primeiro clique do usuário ---
primeAudioContext();
const password = passwordInput.value;
errorMessage.textContent = '';
try {
const response = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password })
});
if (response.ok) {
loginPage.classList.add('hidden');
chatPage.classList.remove('hidden');
chatPage.classList.add('flex');
} else {
errorMessage.textContent = 'Senha incorreta. Tente novamente.';
}
} catch (error) {
console.error('Erro de conexão:', error);
errorMessage.textContent = 'Não foi possível conectar ao servidor.';
}
}
// --- Lógica de Gravação ---
recordButton.addEventListener('click', () => {
if (isRecording) {
stopRecording();
} else {
startRecording();
}
});
async function startRecording() {
audioChunks = [];
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4';
mediaRecorder = new MediaRecorder(stream, { mimeType });
mediaRecorder.ondataavailable = event => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = sendAudioToServer;
mediaRecorder.start();
isRecording = true;
setButtonState('recording');
} catch (error) {
console.error("ERRO AO INICIAR GRAVAÇÃO:", error.name, error.message);
let userMessage = 'Ocorreu um erro ao tentar aceder ao microfone. Verifique as permissões.';
if (error.name === 'NotAllowedError') {
userMessage = 'A permissão para usar o microfone foi negada. Por favor, habilite nas configurações do seu navegador.';
} else if (error.name === 'NotFoundError') {
userMessage = 'Nenhum microfone foi encontrado no seu dispositivo.';
}
addMessageToChat(userMessage, "error");
setButtonState('idle');
}
}
function stopRecording() {
if (!isRecording || !mediaRecorder) return;
mediaRecorder.stop();
isRecording = false;
mediaRecorder.stream.getTracks().forEach(track => track.stop());
setButtonState('processing');
}
// --- Comunicação com o Servidor ---
async function sendAudioToServer() {
const audioBlob = new Blob(audioChunks, { type: mediaRecorder.mimeType });
if (audioBlob.size < 1000) {
setButtonState('idle');
addMessageToChat('A gravação foi muito curta. Tente novamente.', 'error');
return;
}
const formData = new FormData();
const fileName = mediaRecorder.mimeType.includes('webm') ? 'recording.webm' : 'recording.mp4';
formData.append('audio', audioBlob, fileName);
try {
const response = await fetch('/process-audio', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Erro do servidor: ${response.statusText}`);
}
const data = await response.json();
addMessageToChat(data.user_question, 'user');
addMessageToChat(data.ai_answer, 'ai');
if (data.audio_base64) {
playAudioAutomatically(data.audio_base64);
} else {
setButtonState('idle');
}
} catch (error) {
console.error('Erro ao processar áudio:', error);
addMessageToChat(error.message || 'Ocorreu um erro. Tente novamente.', 'error');
setButtonState('idle');
}
}
// --- Reprodução Automática ---
function playAudioAutomatically(base64String) {
try {
const audioBytes = Uint8Array.from(atob(base64String), c => c.charCodeAt(0));
const audioBlob = new Blob([audioBytes], { type: 'audio/mpeg' });
if (audioPlayer.src && audioPlayer.src.startsWith('blob:')) {
URL.revokeObjectURL(audioPlayer.src);
}
audioPlayer.src = URL.createObjectURL(audioBlob);
const playPromise = audioPlayer.play();
if (playPromise !== undefined) {
playPromise.then(() => {
setButtonState('speaking');
}).catch(error => {
console.error("Falha na reprodução automática:", error);
addMessageToChat("Não foi possível reproduzir o áudio automaticamente.", 'error');
setButtonState('idle');
});
}
audioPlayer.onended = () => {
setButtonState('idle');
};
} catch (error) {
console.error("Erro ao decodificar o áudio:", error);
addMessageToChat("Falha ao processar o áudio.", "error");
setButtonState('idle');
}
}
// --- Funções Auxiliares da UI ---
function setButtonState(state) {
recordButton.classList.remove('recording-pulse', 'bg-red-500', 'bg-teal-500', 'bg-gray-400', 'cursor-not-allowed', 'bg-orange-500');
recordButton.disabled = false;
recordIcon.setAttribute('data-lucide', 'mic');
switch(state) {
case 'recording':
statusText.textContent = 'Gravando... Clique para parar.';
recordButton.classList.add('bg-red-500', 'recording-pulse');
recordIcon.setAttribute('data-lucide', 'square');
break;
case 'processing':
statusText.textContent = 'Processando...';
recordButton.classList.add('bg-gray-400', 'cursor-not-allowed');
recordButton.disabled = true;
break;
case 'speaking':
statusText.textContent = 'O assistente está falando...';
recordButton.classList.add('bg-orange-500');
recordButton.disabled = true;
break;
case 'idle':
default:
statusText.textContent = 'Clique para falar';
recordButton.classList.add('bg-teal-500');
break;
}
lucide.createIcons();
}
function addMessageToChat(text, sender) {
const messageWrapper = document.createElement('div');
const messageBubble = document.createElement('div');
const textElement = document.createElement('p');
messageBubble.classList.add('px-4', 'py-3', 'rounded-xl', 'max-w-lg');
textElement.classList.add('text-sm');
if(!text || text.trim() === "") return;
textElement.textContent = text;
messageBubble.appendChild(textElement);
if (sender === 'user') {
messageWrapper.classList.add('flex', 'justify-end');
messageBubble.classList.add('bg-teal-600', 'text-white');
} else if (sender === 'error') {
messageWrapper.classList.add('flex', 'justify-center');
messageBubble.classList.add('bg-red-100', 'text-red-700');
} else {
messageWrapper.classList.add('flex', 'justify-start');
messageBubble.classList.add('bg-gray-200', 'text-gray-800');
}
messageWrapper.appendChild(messageBubble);
chatHistory.appendChild(messageWrapper);
chatHistory.scrollTop = chatHistory.scrollHeight;
lucide.createIcons();
}
</script>
</body>
</html>