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