EnglishStudyHelper / index.html
amauricunha's picture
Update index.html
b442dad verified
Raw
History Blame
31.8 kB
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic English Study Studio</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
body { font-family: 'Inter', sans-serif; background-color: #f7f9fc; }
.card { background-color: white; border-radius: 12px; box-shadow: 0 4px_12px rgba(0,0,0,0.08); padding: 24px; }
.btn { padding: 0.5rem 1rem; border-radius: 8px; font-weight: 600; transition: all 0.2s; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
.btn:disabled { cursor: not-allowed; opacity: 0.5; }
.btn-primary { background-color: #4f46e5; color: white; }
.btn-primary:hover:not(:disabled) { background-color: #4338ca; }
.spinner { border: 2px solid rgba(255,255,255,0.3); width: 20px; height: 20px; border-radius: 50%; border-left-color: #fff; animation: spin 1s ease infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.flashcard-container { perspective: 1000px; position: relative; }
.flashcard-inner { position: relative; width: 100%; transition: transform 0.6s; transform-style: preserve-3d; }
.flashcard-container.flipped .flashcard-inner { transform: rotateY(180deg); }
.flashcard-front, .flashcard-back { position: absolute; width: 100%; height:100%; backface-visibility: hidden; border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; display: flex; flex-direction: column; justify-content: space-between; }
.flashcard-front { background-color: #f9fafb; }
.flashcard-back { background-color: #ffffff; transform: rotateY(180deg); }
.audio-btn { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #4f46e5; padding: 0; }
#chatDisplay { height: 250px; overflow-y: auto; background-color: #f9fafb; border-radius: 8px; padding: 10px; border: 1px solid #e5e7eb; }
.chat-message { max-width: 80%; padding: 8px 12px; border-radius: 18px; margin-bottom: 8px; line-height: 1.4; word-wrap: break-word; }
.user-message { background-color: #4f46e5; color: white; align-self: flex-end; margin-left: auto; border-bottom-right-radius: 4px; }
.ai-message { background-color: #e5e7eb; color: #1f2937; align-self: flex-start; border-bottom-left-radius: 4px; }
.mic-button.is-recording { background-color: #ef4444; }
.delete-btn { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #9ca3af; z-index: 10; }
.delete-btn:hover { color: #ef4444; }
</style>
</head>
<body class="p-4 md:p-8">
<div class="container mx-auto max-w-5xl">
<header class="text-center mb-10">
<h1 class="text-3xl lg:text-4xl font-bold text-gray-800">Dynamic English Study Studio</h1>
<p class="text-gray-500 mt-2">Sua ferramenta de estudo com áudio, imagens, e prática de conversação e pronúncia.</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Coluna da Esquerda: Inputs -->
<div class="flex flex-col gap-6">
<div class="card">
<h2 class="text-xl font-semibold mb-4 text-gray-700">1. Estudo por Texto</h2>
<div id="textEditor" contenteditable="true" spellcheck="true" lang="en" class="w-full p-4 border rounded-md min-h-[150px]"></div>
<p class="text-sm text-right text-gray-500 mt-2">Caracteres: <span id="charCount">0</span>/10000</p>
<div class="mt-4 border-t pt-4 flex flex-wrap gap-2 justify-between items-center">
<button id="createCardButton" onclick="createFlashcardFromSelection()" class="btn btn-primary bg-green-600 hover:bg-green-700 disabled:opacity-50" disabled>+ Criar Flashcard</button>
<button id="playButton" onclick="handlePlay()" class="btn btn-primary" disabled>
<span id="playText">Ouvir Texto</span>
<div id="playSpinner" class="spinner ml-2 hidden"></div>
</button>
</div>
<audio id="audioPlayer" controls class="w-full mt-4 hidden"></audio>
</div>
<div class="card">
<h2 class="text-xl font-semibold mb-4 text-gray-700">2. Estudo por Imagem (Análise)</h2>
<input type="file" id="imageUploader" accept="image/*" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:font-semibold file:bg-violet-50 file:text-violet-700 hover:file:bg-violet-100"/>
<img id="imagePreview" src="" class="mt-4 rounded-lg hidden max-h-60 mx-auto" alt="Pré-visualização"/>
<div id="imageAnalysisResult" class="mt-4"></div>
</div>
<div class="card">
<h2 class="text-xl font-semibold mb-4 text-gray-700">3. Prática de Conversação</h2>
<div id="chatDisplay" class="flex flex-col space-y-2 mb-4"></div>
<div class="flex gap-2">
<input type="text" id="chatInput" class="flex-grow border rounded-lg px-3 py-2" placeholder="Digite ou fale no microfone...">
<button onclick="handleTextInput()" class="btn btn-primary bg-gray-500 hover:bg-gray-600">Enviar</button>
<button id="micButton" onclick="handleVoiceInput()" class="btn btn-primary mic-button w-12 h-12 rounded-full">🎙️</button>
</div>
</div>
<div class="card">
<h2 class="text-xl font-semibold mb-4 text-gray-700">4. Gerador de Imagens (Nano Banana)</h2>
<p class="text-sm text-gray-600 mb-2">Descreva uma cena em inglês para a IA desenhar.</p>
<div class="flex gap-2">
<input type="text" id="imagePromptInput" class="flex-grow border rounded-lg px-3 py-2" placeholder="Ex: a blue cat reading a book on the moon">
<button id="generateImageBtn" onclick="generateImage()" class="btn btn-primary">Gerar</button>
</div>
<div id="imageResultContainer" class="mt-4 p-4 border rounded-lg bg-gray-50 min-h-[200px] flex items-center justify-center">
<p class="text-gray-500 italic">Sua imagem aparecerá aqui.</p>
</div>
</div>
<div class="card">
<h2 class="text-xl font-semibold mb-4 text-gray-700">5. Atividades de Escrita e Tradução</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label for="activityTypeSelector" class="block text-sm font-medium text-gray-700">Tipo de Atividade</label>
<select id="activityTypeSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
<option value="translation_pt_en">Tradução (PT para EN)</option>
<option value="email_writing">Escrever E-mail Formal</option>
<option value="summary_writing">Resumir o Texto</option>
</select>
</div>
<div class="flex items-end">
<button id="generateActivityButton" onclick="generateActivity()" class="btn btn-primary w-full">Gerar Atividade</button>
</div>
</div>
<div class="mt-4 border-t pt-4">
<button id="translateFullTextButton" onclick="translateFullText()" class="btn btn-primary bg-gray-500 hover:bg-gray-600 w-full" disabled>Traduzir Texto Completo para PT</button>
</div>
<div id="activityOutput" class="mt-4 p-4 border rounded-lg bg-gray-50 min-h-[100px] whitespace-pre-wrap">
<p class="text-gray-500 italic">Seu exercício aparecerá aqui.</p>
</div>
</div>
<div class="card">
<h2 class="text-xl font-semibold mb-4 text-gray-700">Configurações de IA</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="modelSelector" class="block text-sm font-medium text-gray-700">Modelo de IA (Flashcards)</label>
<select id="modelSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
<optgroup label="Google Gemini">
<option value="gemini:gemini-1.5-flash-latest">Gemini 2.5 Flash</option>
<option value="gemini:gemini-1.5-pro-latest">Gemini 2.5 Pro</option>
</optgroup>
<optgroup label="Groq (Ultra Rápido)">
<option value="groq:llama-3.1-8b-instant">Llama 3.1 8B</option>
<option value="groq:llama-3.1-70b-versatile">Llama 3.1 70B</option>
<option value="groq:openai/gpt-oss-120b">OpenAI GPT-OSS 120B</option>
</optgroup>
</select>
</div>
<div>
<label for="contextSelector" class="block text-sm font-medium text-gray-700">Foco do Vocabulário</label>
<select id="contextSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
<option value="General/Social">Geral/Social</option>
<option value="Professional/Business">Profissional/Negócios</option>
<option value="Technical/IT">Técnico/TI</option>
</select>
</div>
</div>
</div>
</div>
<!-- Coluna da Direita: Flashcards -->
<div id="flashcardsPanel" class="card">
<h2 class="text-xl font-semibold mb-4 text-gray-700">Sua Sessão de Estudo</h2>
<div id="flashcardList" class="space-y-6">
<p id="noCardsMessage" class="text-gray-500 italic">Nenhum flashcard criado ainda.</p>
</div>
</div>
</div>
</div>
<script>
// --- ELEMENTOS GLOBAIS ---
const textEditor = document.getElementById('textEditor');
const charCount = document.getElementById('charCount');
const createCardButton = document.getElementById('createCardButton');
const playButton = document.getElementById('playButton');
const playText = document.getElementById('playText');
const playSpinner = document.getElementById('playSpinner');
const audioPlayer = document.getElementById('audioPlayer');
const flashcardList = document.getElementById('flashcardList');
const noCardsMessage = document.getElementById('noCardsMessage');
const imageUploader = document.getElementById('imageUploader');
const imagePreview = document.getElementById('imagePreview');
const imageAnalysisResult = document.getElementById('imageAnalysisResult');
const chatDisplay = document.getElementById('chatDisplay');
const chatInput = document.getElementById('chatInput');
const micButton = document.getElementById('micButton');
const modelSelector = document.getElementById('modelSelector');
const contextSelector = document.getElementById('contextSelector');
const imagePromptInput = document.getElementById('imagePromptInput');
const generateImageBtn = document.getElementById('generateImageBtn');
const imageResultContainer = document.getElementById('imageResultContainer');
const activityTypeSelector = document.getElementById('activityTypeSelector');
const generateActivityButton = document.getElementById('generateActivityButton');
const activityOutput = document.getElementById('activityOutput');
const translateFullTextButton = document.getElementById('translateFullTextButton');
let chatHistory = [];
let recognition;
let isRecording = false;
// --- LÓGICA PRINCIPAL DO EDITOR DE TEXTO ---
textEditor.addEventListener('input', () => {
const len = textEditor.innerText.length;
charCount.textContent = `${len}/10000`;
playButton.disabled = len === 0;
translateFullTextButton.disabled = len === 0;
});
textEditor.addEventListener('mouseup', () => {
createCardButton.disabled = !window.getSelection().toString().trim();
});
// --- LÓGICA DE ÁUDIO DO TEXTO PRINCIPAL ---
async function handlePlay() {
const text = textEditor.innerText;
if (!text) return;
playButton.disabled = true;
playText.textContent = 'Gerando...';
playSpinner.classList.remove('hidden');
try {
const audioBlob = await fetchAudioBlob(text);
const audioUrl = URL.createObjectURL(audioBlob);
audioPlayer.src = audioUrl;
audioPlayer.classList.remove('hidden');
audioPlayer.play();
audioPlayer.onended = () => URL.revokeObjectURL(audioUrl);
} catch (error) {
alert(`Erro ao gerar áudio: ${error.message}. Verifique o console para mais detalhes.`);
} finally {
playButton.disabled = false;
playText.textContent = 'Ouvir Texto';
playSpinner.classList.add('hidden');
}
}
// --- LÓGICA DE CONVERSAÇÃO (VOZ E TEXTO) ---
function initializeSpeechRecognition() {
window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!window.SpeechRecognition) {
micButton.disabled = true;
micButton.textContent = '❌';
alert('A API de reconhecimento de voz não é suportada neste navegador.');
return;
}
recognition = new SpeechRecognition();
recognition.lang = 'en-US';
recognition.interimResults = false;
recognition.continuous = false;
recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
chatInput.value = transcript;
sendMessage(transcript);
};
recognition.onerror = (event) => { console.error('Erro no reconhecimento de voz:', event.error); };
recognition.onend = () => {
isRecording = false;
micButton.classList.remove('is-recording');
micButton.innerHTML = '🎙️';
};
}
function handleVoiceInput() {
if (isRecording) {
recognition.stop();
} else {
isRecording = true;
micButton.classList.add('is-recording');
micButton.innerHTML = '<div class="spinner"></div>';
recognition.start();
}
}
function handleTextInput() {
const message = chatInput.value.trim();
sendMessage(message);
chatInput.value = '';
}
chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleTextInput(); });
async function sendMessage(message) {
if (!message) return;
appendMessage(message, 'user');
chatHistory.push({ role: 'user', content: message });
try {
const response = await fetch('/chat-with-ai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ history: chatHistory, message: message })
});
if (!response.ok) throw new Error((await response.json()).error);
const data = await response.json();
appendMessage(data.response, 'ai');
chatHistory.push({ role: 'assistant', content: data.response });
playAudio(data.response);
} catch (error) {
appendMessage(`Erro na conversa: ${error.message}`, 'ai');
}
}
function appendMessage(text, sender) {
const msg = document.createElement('div');
msg.className = `chat-message ${sender}-message`;
msg.textContent = text;
chatDisplay.appendChild(msg);
chatDisplay.scrollTop = chatDisplay.scrollHeight;
}
// --- LÓGICA DE ESTUDO POR IMAGEM (ANÁLISE) ---
imageUploader.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.src = e.target.result;
imagePreview.classList.remove('hidden');
analyzeImage(e.target.result);
};
reader.readAsDataURL(file);
});
async function analyzeImage(base64Image) {
imageAnalysisResult.innerHTML = `<div class="flex items-center p-2"><div class="spinner !border-l-indigo-500 !border-gray-200"></div><p class="ml-3 text-gray-600">Analisando imagem...</p></div>`;
try {
const response = await fetch('/analyze-image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64Image })
});
if (!response.ok) throw new Error((await response.json()).error);
const vocabulary = await response.json();
displayImageVocabulary(vocabulary);
} catch (error) {
imageAnalysisResult.innerHTML = `<p class="text-red-600 font-semibold">Falha na análise: ${error.message}</p>`;
}
}
function displayImageVocabulary(vocabulary) {
imageAnalysisResult.innerHTML = '<h3 class="font-semibold mb-2">Vocabulário Sugerido:</h3>';
const buttonContainer = document.createElement('div');
buttonContainer.className = 'flex flex-wrap gap-2';
vocabulary.forEach(({ term, definition }) => {
const btn = document.createElement('button');
btn.className = 'btn bg-violet-100 text-violet-700 hover:bg-violet-200 text-sm';
btn.textContent = term;
btn.onclick = () => createFlashcardFromSuggestion(term, definition);
buttonContainer.appendChild(btn);
});
imageAnalysisResult.appendChild(buttonContainer);
}
// --- LÓGICA DE GERAÇÃO DE IMAGEM (NANO BANANA) ---
async function generateImage() {
const prompt = imagePromptInput.value.trim();
if (!prompt) {
alert('Por favor, digite uma descrição para a imagem.');
return;
}
generateImageBtn.disabled = true;
generateImageBtn.innerHTML = '<div class="spinner mr-2"></div> Gerando...';
imageResultContainer.innerHTML = `<div class="flex items-center justify-center p-4"><div class="spinner !border-l-indigo-500 !border-gray-200"></div><p class="ml-3 text-gray-600">Criando sua imagem...</p></div>`;
try {
const response = await fetch('/generate-image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: prompt })
});
if (!response.ok) throw new Error((await response.json()).error);
const data = await response.json();
imageResultContainer.innerHTML = `<img src="data:image/png;base64,${data.image_base64}" class="rounded-lg max-h-60 mx-auto" alt="Imagem gerada por IA"/>`;
} catch (error) {
imageResultContainer.innerHTML = `<p class="text-red-600 font-semibold">Falha ao gerar imagem: ${error.message}</p>`;
} finally {
generateImageBtn.disabled = false;
generateImageBtn.textContent = 'Gerar';
}
}
// --- LÓGICA DE ATIVIDADES DE ESCRITA E TRADUÇÃO ---
async function generateActivity() {
const activityType = activityTypeSelector.value;
const context = contextSelector.value;
const sourceText = textEditor.innerText;
generateActivityButton.disabled = true;
generateActivityButton.innerHTML = '<div class="spinner mr-2"></div> Gerando...';
activityOutput.innerHTML = '<p class="text-gray-500 italic">Gerando seu exercício...</p>';
let prompt = "";
if (activityType === 'translation_pt_en') {
prompt = `Baseado no contexto '${context}', crie uma frase em português para eu traduzir para o inglês.`;
} else if (activityType === 'email_writing') {
prompt = `Crie um cenário para eu escrever um e-mail formal, relacionado ao contexto '${context}'.`;
} else if (activityType === 'summary_writing') {
if (!sourceText) {
alert("Por favor, cole um texto no editor para gerar a atividade de resumo.");
activityOutput.innerHTML = '<p class="text-red-500">Cole um texto acima primeiro.</p>';
generateActivityButton.disabled = false;
generateActivityButton.textContent = 'Gerar Atividade';
return;
}
prompt = `Resuma o seguinte texto em uma frase: "${sourceText}"`;
}
try {
const response = await fetch('/explain-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: modelSelector.value,
context_focus: context,
custom_prompt: prompt
})
});
if (!response.ok) throw new Error((await response.json()).error);
const data = await response.json();
activityOutput.textContent = data.explanation;
} catch (error) {
activityOutput.innerHTML = `<p class="text-red-600 font-semibold">Falha ao gerar atividade: ${error.message}</p>`;
} finally {
generateActivityButton.disabled = false;
generateActivityButton.textContent = 'Gerar Atividade';
}
}
async function translateFullText() {
const text = textEditor.innerText;
if (!text) return;
translateFullTextButton.disabled = true;
translateFullTextButton.innerHTML = '<div class="spinner mr-2"></div> Traduzindo...';
const prompt = `Traduza o seguinte texto para o português de forma clara e natural: "${text}"`;
try {
const response = await fetch('/explain-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: modelSelector.value,
context_focus: contextSelector.value,
custom_prompt: prompt
})
});
if (!response.ok) throw new Error((await response.json()).error);
const data = await response.json();
alert("Tradução Completa:\n\n" + data.explanation);
} catch (error) {
alert(`Falha na tradução: ${error.message}`);
} finally {
translateFullTextButton.disabled = false;
translateFullTextButton.textContent = 'Traduzir Texto Completo para PT';
}
}
// --- LÓGICA DE GERAÇÃO DE FLASHCARD ---
async function createFlashcard(word, context, definition = null) {
const btn = definition ? null : createCardButton;
if (btn) {
btn.disabled = true;
btn.innerHTML = '<div class="spinner mr-2"></div> Criando...';
}
try {
const response = await fetch('/explain-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
word: word,
context: context,
for_flashcard: true,
model: modelSelector.value,
context_focus: contextSelector.value
})
});
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.error || 'Erro desconhecido do servidor.');
}
const cardData = await response.json();
if(definition && !cardData.definition) cardData.definition = definition;
renderFlashcard(cardData);
} catch (error) {
alert(`Falha ao criar flashcard: ${error.message}. Verifique as chaves de API e a seleção do modelo.`);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '+ Criar Flashcard';
}
}
}
function createFlashcardFromSelection() {
const selection = window.getSelection().toString().trim();
if (selection) createFlashcard(selection, textEditor.innerText);
}
function createFlashcardFromSuggestion(term, definition) {
createFlashcard(term, `(From image analysis) A visual representation of ${term}.`, definition);
}
function renderFlashcard(data) {
noCardsMessage.classList.add('hidden');
const cardId = `card-${Date.now()}`;
const container = document.createElement('div');
container.className = 'flashcard-container';
container.id = cardId;
const minHeight = 380;
container.style.minHeight = `${minHeight}px`;
const fallback = 'Informação não gerada';
container.innerHTML = `
<button class="delete-btn" onclick="deleteFlashcard('${cardId}', event)">🗑️</button>
<div class="flashcard-inner" style="min-height: ${minHeight}px;">
<div class="flashcard-front">
<div>
<div class="text-sm text-gray-600 mb-2">Frase de Contexto:</div>
<p class="text-lg text-center text-gray-800">${data.gapped_sentence || fallback}</p>
</div>
<div class="border-t pt-4 mt-4 text-center">
<strong class="text-indigo-600">Dica (PT):</strong> ${data.translation || fallback}
</div>
<div class="text-xs text-center text-gray-400 mt-4">Clique para ver a resposta</div>
</div>
<div class="flashcard-back">
<div>
<h3 class="text-xl font-bold text-center text-indigo-600 mb-2">${data.term || 'Termo'}</h3>
<p class="flex items-center justify-center gap-2 text-gray-600 italic mb-3">
<button class="audio-btn" onclick="playAudio('${(data.context_sentence || '').replace(/'/g, "\\'")}', event)">🔊</button>
<span>${data.context_sentence || fallback}</span>
</p>
<p class="text-sm text-gray-800"><strong class="font-semibold">Definição:</strong> ${data.definition || fallback}</p>
</div>
<div class="border-t mt-3 pt-3">
<h4 class="text-sm font-semibold text-center">Pratique sua Pronúncia</h4>
<div class="flex justify-center items-center gap-2 mt-2">
<button class="btn btn-primary w-12 h-12 rounded-full" onclick="handlePronunciationPractice(this, '${(data.context_sentence || '').replace(/'/g, "\\'")}', event)">🎙️</button>
</div>
<div class="text-xs text-gray-600 p-2 mt-2 bg-gray-50 rounded-md min-h-[40px]" data-feedback-area></div>
</div>
</div>
</div>`;
flashcardList.prepend(container);
container.addEventListener('click', (e) => {
if(e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
container.classList.toggle('flipped');
}
});
}
function deleteFlashcard(cardId, event) {
event.stopPropagation();
const cardToDelete = document.getElementById(cardId);
if (cardToDelete) {
cardToDelete.remove();
}
if (flashcardList.children.length === 1) {
noCardsMessage.classList.remove('hidden');
}
}
async function handlePronunciationPractice(button, targetText, event) {
event.stopPropagation();
const feedbackArea = button.closest('.flashcard-back').querySelector('[data-feedback-area]');
const practiceRecognition = new SpeechRecognition();
practiceRecognition.lang = 'en-US';
button.innerHTML = '<div class="spinner"></div>';
button.disabled = true;
feedbackArea.textContent = 'Ouvindo...';
practiceRecognition.start();
practiceRecognition.onresult = async (e) => {
const userText = e.results[0][0].transcript;
feedbackArea.textContent = `Você disse: "${userText}". Analisando...`;
try {
const response = await fetch('/pronunciation-feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_text: targetText, user_text: userText })
});
if (!response.ok) throw new Error((await response.json()).error);
const data = await response.json();
feedbackArea.innerHTML = `<strong>Feedback:</strong> ${data.feedback}`;
} catch (error) {
feedbackArea.textContent = `Erro na análise: ${error.message}`;
} finally {
button.innerHTML = '🎙️';
button.disabled = false;
}
};
practiceRecognition.onerror = (e) => {
feedbackArea.textContent = `Erro ao gravar: ${e.error}`;
button.innerHTML = '🎙️';
button.disabled = false;
};
}
// --- FUNÇÕES AUXILIARES DE ÁUDIO ---
async function fetchAudioBlob(text) {
const response = await fetch('/tts-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text })
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error || `HTTP error! status: ${response.status}`);
}
return await response.blob();
}
async function playAudio(text, event) {
if(event) event.stopPropagation();
try {
const audioBlob = await fetchAudioBlob(text);
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.play();
audio.onended = () => URL.revokeObjectURL(audioUrl);
} catch (error) { console.error('Erro ao tocar áudio:', error); }
}
// --- INICIALIZAÇÃO ---
document.addEventListener('DOMContentLoaded', () => {
initializeSpeechRecognition();
});
</script>
</body>
</html>