| <!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"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
|
|