amauricunha's picture
Upload 2 files
6f713c1 verified
Raw
History Blame Contribute Delete
35.5 kB
<!DOCTYPE html>
<html lang="en">
<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>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></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; }
.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; }
#activityChatDisplay { border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; min-height: 150px; background-color: #f9fafb; }
.ai-prompt { border-bottom: 2px dashed #d1d5db; padding-bottom: 1rem; margin-bottom: 1rem; }
.user-response-display { background-color: #eef2ff; border-left: 4px solid #4f46e5; padding: 0.75rem; margin-bottom: 1rem; font-style: italic; }
.ai-feedback { background-color: #f0fdf4; border-left: 4px solid #22c55e; padding: 0.75rem; }
.ai-feedback h3 { font-weight: 700; }
</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">Your study tool with audio, images, and conversation & pronunciation practice.</p>
</header>
<div class="card mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-700">Global Session Settings</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="modelSelector" class="block text-sm font-medium text-gray-700">Main AI Model</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">
<option>Loading models...</option>
</select>
</div>
<div>
<label for="contextSelector" class="block text-sm font-medium text-gray-700">Vocabulary Focus</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">General/Social</option>
<option value="Professional/Business">Professional/Business</option>
<option value="Technical/IT">Technical/IT</option>
</select>
</div>
<div>
<label for="voiceSelector" class="block text-sm font-medium text-gray-700">Voice Accent (TTS)</label>
<select id="voiceSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
<option value="co.uk">British (Female)</option>
<option value="com">American (Female)</option>
<option value="com.au">Australian (Female)</option>
</select>
</div>
</div>
</div>
<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. Study from Text</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">Characters: <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>+ Create Flashcard</button>
<button id="playButton" onclick="handlePlay()" class="btn btn-primary" disabled>
<span id="playText">Listen to Text</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. Study from Image (Analysis)</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="Preview"/>
<div id="imageAnalysisResult" class="mt-4"></div>
</div>
<div class="card">
<h2 class="text-xl font-semibold mb-4 text-gray-700">3. Conversation Practice</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="Type or use the microphone...">
<button onclick="handleTextInput()" class="btn btn-primary bg-gray-500 hover:bg-gray-600">Send</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. Image Generator (Nano Banana)</h2>
<p class="text-sm text-gray-600 mb-2">Describe a scene in English for the AI to draw.</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">Generate</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">Your image will appear here.</p>
</div>
</div>
<div class="card">
<h2 class="text-xl font-semibold mb-4 text-gray-700">5. Interactive Writing Practice</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">Activity Type</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="translate_pt_en">Translate (PT to EN)</option>
<option value="email_writing">Write Formal Email</option>
<option value="summary_writing">Summarize Main Text</option>
<option value="professional_conversation">Simulate Professional Conversation</option>
</select>
</div>
<div class="flex items-end">
<button id="generateActivityButton" onclick="generateActivity()" class="btn btn-primary w-full">Generate Activity</button>
</div>
</div>
<div id="activityChatContainer" class="mt-4 border-t pt-4">
<div id="activityChatDisplay">
<p class="text-gray-500 italic">Your interactive exercise will appear here.</p>
</div>
<div id="activityResponseArea" class="mt-4 hidden">
<label for="activityInput" class="block text-sm font-medium text-gray-700 mb-1">Your Answer:</label>
<textarea id="activityInput" rows="4" class="w-full p-2 border rounded-md"></textarea>
<button id="submitActivityBtn" onclick="submitActivityResponse()" class="btn btn-primary mt-2">Submit Answer</button>
</div>
</div>
</div>
</div>
<!-- Coluna da Direita: Flashcards -->
<div id="flashcardsPanel" class="card">
<h2 class="text-xl font-semibold mb-4 text-gray-700">Your Study Session</h2>
<div id="flashcardList" class="space-y-6">
<p id="noCardsMessage" class="text-gray-500 italic">No flashcards created yet.</p>
</div>
</div>
</div>
</div>
<script>
// --- ELEMENTOS GLOBAIS ---
const modelSelector = document.getElementById('modelSelector');
const contextSelector = document.getElementById('contextSelector');
const voiceSelector = document.getElementById('voiceSelector');
const textEditor = document.getElementById('textEditor');
const createCardButton = document.getElementById('createCardButton');
const playButton = document.getElementById('playButton');
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 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 activityChatDisplay = document.getElementById('activityChatDisplay');
const activityResponseArea = document.getElementById('activityResponseArea');
const activityInput = document.getElementById('activityInput');
const submitActivityBtn = document.getElementById('submitActivityBtn');
let chatHistory = [];
let recognition;
let isRecording = false;
let currentActivityPrompt = "";
// --- CARREGAMENTO DINÂMICO DE MODELOS ---
async function populateModelSelector() {
try {
const response = await fetch('/list-models');
if (!response.ok) throw new Error('Failed to fetch model list.');
const models = await response.json();
modelSelector.innerHTML = '';
const geminiGroup = document.createElement('optgroup');
geminiGroup.label = 'Google Gemini';
const groqGroup = document.createElement('optgroup');
groqGroup.label = 'Groq (Ultra Fast)';
models.forEach(model => {
const option = document.createElement('option');
option.value = model.value;
option.textContent = model.name;
if (model.value.startsWith('gemini:')) {
geminiGroup.appendChild(option);
} else if (model.value.startsWith('groq:')) {
groqGroup.appendChild(option);
}
});
modelSelector.appendChild(geminiGroup);
modelSelector.appendChild(groqGroup);
} catch (error) {
console.error("Error populating models:", error);
modelSelector.innerHTML = '<option>Error loading models</option>';
}
}
// --- FUNÇÕES DE ÁUDIO ---
async function fetchAudioBlob(text) {
const selectedTld = voiceSelector.value;
const response = await fetch('/tts-proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text, tld: selectedTld })
});
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('Error playing audio:', error); }
}
// --- LÓGICA DO EDITOR ---
const charCount = document.getElementById('charCount');
const playText = document.getElementById('playText');
const playSpinner = document.getElementById('playSpinner');
textEditor.addEventListener('input', () => {
const len = textEditor.innerText.length;
charCount.textContent = `${len}/10000`;
playButton.disabled = len === 0;
});
textEditor.addEventListener('mouseup', () => {
createCardButton.disabled = !window.getSelection().toString().trim();
});
async function handlePlay() {
const text = textEditor.innerText;
if (!text) return;
playButton.disabled = true;
playText.textContent = 'Generating...';
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(`Error generating audio: ${error.message}. Check the console for more details.`);
} finally {
playButton.disabled = false;
playText.textContent = 'Listen to Text';
playSpinner.classList.add('hidden');
}
}
// --- TODAS AS OUTRAS FUNÇÕES ---
// ATIVIDADES
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> Generating...';
activityChatDisplay.innerHTML = '<p class="text-gray-500 italic">Generating your exercise...</p>';
activityResponseArea.classList.add('hidden');
let prompt = "";
if (activityType === 'translate_pt_en') {
prompt = `Based on the '${context}' context, create one single sentence in Portuguese for me to translate into English.`;
} else if (activityType === 'email_writing') {
prompt = `Create a scenario for me to write a formal email, related to the '${context}' context. Give me clear instructions on what to write.`;
} else if (activityType === 'summary_writing') {
if (!sourceText) {
alert("Please paste text in the main editor to generate a summary activity.");
activityChatDisplay.innerHTML = '<p class="text-red-500">Please paste text in the editor in card #1 first.</p>';
generateActivityButton.disabled = false;
generateActivityButton.textContent = 'Generate Activity';
return;
}
prompt = `Your task is to summarize the following text in 2 or 3 sentences: "${sourceText}"`;
} else if (activityType === 'professional_conversation') {
prompt = `Start a professional role-playing conversation with me related to '${context}'. Ask me an open-ended question to begin.`;
}
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();
currentActivityPrompt = data.explanation;
activityChatDisplay.innerHTML = `<div class="ai-prompt"><strong>Your Task:</strong><p>${currentActivityPrompt}</p></div>`;
activityResponseArea.classList.remove('hidden');
} catch (error) {
activityChatDisplay.innerHTML = `<p class="text-red-600 font-semibold">Failed to generate activity: ${error.message}</p>`;
} finally {
generateActivityButton.disabled = false;
generateActivityButton.textContent = 'Generate Activity';
}
}
async function submitActivityResponse() {
const userResponse = activityInput.value.trim();
if (!userResponse) {
alert("Please write a response.");
return;
}
submitActivityBtn.disabled = true;
submitActivityBtn.innerHTML = '<div class="spinner mr-2"></div> Getting Feedback...';
const oldFeedback = activityChatDisplay.querySelector('.ai-feedback');
if(oldFeedback) oldFeedback.remove();
const oldResponse = activityChatDisplay.querySelector('.user-response-display');
if(oldResponse) oldResponse.remove();
const userResponseDiv = document.createElement('div');
userResponseDiv.className = 'user-response-display';
userResponseDiv.innerHTML = `<strong>Your Response:</strong><p>${userResponse}</p>`;
activityChatDisplay.appendChild(userResponseDiv);
try {
const response = await fetch('/activity-feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: modelSelector.value,
context_focus: contextSelector.value,
original_prompt: currentActivityPrompt,
user_response: userResponse
})
});
if (!response.ok) throw new Error((await response.json()).error);
const data = await response.json();
const feedbackDiv = document.createElement('div');
feedbackDiv.className = 'ai-feedback';
feedbackDiv.innerHTML = `<strong>Feedback:</strong><div class="prose prose-sm max-w-none">${marked.parse(data.feedback)}</div>`;
activityChatDisplay.appendChild(feedbackDiv);
activityInput.value = '';
} catch (error) {
const errorDiv = document.createElement('div');
errorDiv.className = 'ai-feedback';
errorDiv.innerHTML = `<p class="text-red-600 font-semibold">Failed to get feedback: ${error.message}</p>`;
activityChatDisplay.appendChild(errorDiv);
} finally {
submitActivityBtn.disabled = false;
submitActivityBtn.textContent = 'Submit Answer';
}
}
// CONVERSAÇÃO
function initializeSpeechRecognition() {
window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!window.SpeechRecognition) {
micButton.disabled = true;
micButton.textContent = '❌';
alert('Voice recognition API is not supported in this browser.');
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('Voice recognition error:', 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(`Chat error: ${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;
}
// IMAGEM
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">Analyzing image...</p></div>`;
try {
const response = await fetch('/analyze-image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image: base64Image,
model: modelSelector.value
})
});
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">Analysis failed: ${error.message}</p>`;
}
}
function displayImageVocabulary(vocabulary) {
imageAnalysisResult.innerHTML = '<h3 class="font-semibold mb-2">Suggested Vocabulary:</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);
}
async function generateImage() {
const prompt = imagePromptInput.value.trim();
if (!prompt) {
alert('Please enter a description for the image.');
return;
}
generateImageBtn.disabled = true;
generateImageBtn.innerHTML = '<div class="spinner mr-2"></div> Generating...';
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">Creating your image...</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="AI-generated image"/>`;
} catch (error) {
imageResultContainer.innerHTML = `<p class="text-red-600 font-semibold">Failed to generate image: ${error.message}</p>`;
} finally {
generateImageBtn.disabled = false;
generateImageBtn.textContent = 'Generate';
}
}
// FLASHCARDS
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> Creating...';
}
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 || 'Unknown server error.');
}
const cardData = await response.json();
if(definition && !cardData.definition) cardData.definition = definition;
renderFlashcard(cardData);
} catch (error) {
alert(`Failed to create flashcard: ${error.message}. Check API keys and model selection.`);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '+ Create 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 = 'Info not generated';
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">Context Sentence:</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">Hint (PT):</strong> ${data.translation || fallback}
</div>
<div class="text-xs text-center text-gray-400 mt-4">Click to see the answer</div>
</div>
<div class="flashcard-back">
<div>
<h3 class="text-xl font-bold text-center text-indigo-600 mb-2">${data.term || 'Term'}</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">Definition:</strong> ${data.definition || fallback}</p>
</div>
<div class="border-t mt-3 pt-3">
<h4 class="text-sm font-semibold text-center">Practice Your Pronunciation</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 = 'Listening...';
practiceRecognition.start();
practiceRecognition.onresult = async (e) => {
const userText = e.results[0][0].transcript;
feedbackArea.textContent = `You said: "${userText}". Analyzing...`;
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 = `Analysis error: ${error.message}`;
} finally {
button.innerHTML = '🎙️';
button.disabled = false;
}
};
practiceRecognition.onerror = (e) => {
feedbackArea.textContent = `Recording error: ${e.error}`;
button.innerHTML = '🎙️';
button.disabled = false;
};
}
// --- INICIALIZAÇÃO ---
document.addEventListener('DOMContentLoaded', () => {
populateModelSelector();
initializeSpeechRecognition();
});
</script>
</body>
</html>