amauricunha commited on
Commit
a9007fe
Β·
verified Β·
1 Parent(s): 202e2b1

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +231 -198
index.html CHANGED
@@ -1,10 +1,12 @@
1
  <!DOCTYPE html>
2
- <html lang="pt-BR">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Dynamic English Study Studio</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
 
 
8
  <style>
9
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
10
  body { font-family: 'Inter', sans-serif; background-color: #f7f9fc; }
@@ -22,13 +24,14 @@
22
  .flashcard-front { background-color: #f9fafb; }
23
  .flashcard-back { background-color: #ffffff; transform: rotateY(180deg); }
24
  .audio-btn { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #4f46e5; padding: 0; }
25
- #chatDisplay { height: 250px; overflow-y: auto; background-color: #f9fafb; border-radius: 8px; padding: 10px; border: 1px solid #e5e7eb; }
26
- .chat-message { max-width: 80%; padding: 8px 12px; border-radius: 18px; margin-bottom: 8px; line-height: 1.4; word-wrap: break-word; }
27
- .user-message { background-color: #4f46e5; color: white; align-self: flex-end; margin-left: auto; border-bottom-right-radius: 4px; }
28
- .ai-message { background-color: #e5e7eb; color: #1f2937; align-self: flex-start; border-bottom-left-radius: 4px; }
29
- .mic-button.is-recording { background-color: #ef4444; }
30
  .delete-btn { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #9ca3af; z-index: 10; }
31
  .delete-btn:hover { color: #ef4444; }
 
 
 
 
 
 
32
  </style>
33
  </head>
34
  <body class="p-4 md:p-8">
@@ -36,20 +39,48 @@
36
  <div class="container mx-auto max-w-5xl">
37
  <header class="text-center mb-10">
38
  <h1 class="text-3xl lg:text-4xl font-bold text-gray-800">Dynamic English Study Studio</h1>
39
- <p class="text-gray-500 mt-2">Sua ferramenta de estudo com Γ‘udio, imagens, e prΓ‘tica de conversaΓ§Γ£o e pronΓΊncia.</p>
40
  </header>
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
43
- <!-- Coluna da Esquerda: Inputs -->
44
  <div class="flex flex-col gap-6">
45
  <div class="card">
46
- <h2 class="text-xl font-semibold mb-4 text-gray-700">1. Estudo por Texto</h2>
47
  <div id="textEditor" contenteditable="true" spellcheck="true" lang="en" class="w-full p-4 border rounded-md min-h-[150px]"></div>
48
- <p class="text-sm text-right text-gray-500 mt-2">Caracteres: <span id="charCount">0</span>/10000</p>
49
  <div class="mt-4 border-t pt-4 flex flex-wrap gap-2 justify-between items-center">
50
- <button id="createCardButton" onclick="createFlashcardFromSelection()" class="btn btn-primary bg-green-600 hover:bg-green-700 disabled:opacity-50" disabled>+ Criar Flashcard</button>
51
  <button id="playButton" onclick="handlePlay()" class="btn btn-primary" disabled>
52
- <span id="playText">Ouvir Texto</span>
53
  <div id="playSpinner" class="spinner ml-2 hidden"></div>
54
  </button>
55
  </div>
@@ -57,105 +88,83 @@
57
  </div>
58
 
59
  <div class="card">
60
- <h2 class="text-xl font-semibold mb-4 text-gray-700">2. Estudo por Imagem (AnΓ‘lise)</h2>
61
  <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"/>
62
- <img id="imagePreview" src="" class="mt-4 rounded-lg hidden max-h-60 mx-auto" alt="PrΓ©-visualizaΓ§Γ£o"/>
63
  <div id="imageAnalysisResult" class="mt-4"></div>
64
  </div>
65
 
66
  <div class="card">
67
- <h2 class="text-xl font-semibold mb-4 text-gray-700">3. PrΓ‘tica de ConversaΓ§Γ£o</h2>
68
  <div id="chatDisplay" class="flex flex-col space-y-2 mb-4"></div>
69
  <div class="flex gap-2">
70
- <input type="text" id="chatInput" class="flex-grow border rounded-lg px-3 py-2" placeholder="Digite ou fale no microfone...">
71
- <button onclick="handleTextInput()" class="btn btn-primary bg-gray-500 hover:bg-gray-600">Enviar</button>
72
  <button id="micButton" onclick="handleVoiceInput()" class="btn btn-primary mic-button w-12 h-12 rounded-full">πŸŽ™οΈ</button>
73
  </div>
74
  </div>
75
 
76
  <div class="card">
77
- <h2 class="text-xl font-semibold mb-4 text-gray-700">4. Gerador de Imagens (Nano Banana)</h2>
78
- <p class="text-sm text-gray-600 mb-2">Descreva uma cena em inglΓͺs para a IA desenhar.</p>
79
  <div class="flex gap-2">
80
  <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">
81
- <button id="generateImageBtn" onclick="generateImage()" class="btn btn-primary">Gerar</button>
82
  </div>
83
  <div id="imageResultContainer" class="mt-4 p-4 border rounded-lg bg-gray-50 min-h-[200px] flex items-center justify-center">
84
- <p class="text-gray-500 italic">Sua imagem aparecerΓ‘ aqui.</p>
85
  </div>
86
  </div>
87
 
 
88
  <div class="card">
89
- <h2 class="text-xl font-semibold mb-4 text-gray-700">5. Atividades de Escrita e TraduΓ§Γ£o</h2>
90
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
91
  <div>
92
- <label for="activityTypeSelector" class="block text-sm font-medium text-gray-700">Tipo de Atividade</label>
93
  <select id="activityTypeSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
94
- <option value="translation_pt_en">TraduΓ§Γ£o (PT para EN)</option>
95
- <option value="email_writing">Escrever E-mail Formal</option>
96
- <option value="summary_writing">Resumir o Texto</option>
 
97
  </select>
98
  </div>
99
  <div class="flex items-end">
100
- <button id="generateActivityButton" onclick="generateActivity()" class="btn btn-primary w-full">Gerar Atividade</button>
101
  </div>
102
  </div>
103
- <div class="mt-4 border-t pt-4">
104
- <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>
105
- </div>
106
- <div id="activityOutput" class="mt-4 p-4 border rounded-lg bg-gray-50 min-h-[100px] whitespace-pre-wrap">
107
- <p class="text-gray-500 italic">Seu exercΓ­cio aparecerΓ‘ aqui.</p>
 
 
 
 
 
108
  </div>
109
  </div>
110
 
111
-
112
- <div class="card">
113
- <h2 class="text-xl font-semibold mb-4 text-gray-700">ConfiguraΓ§Γ΅es de IA</h2>
114
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
115
- <div>
116
- <label for="modelSelector" class="block text-sm font-medium text-gray-700">Modelo de IA (Flashcards)</label>
117
- <select id="modelSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
118
- <optgroup label="Google Gemini">
119
- <option value="gemini:gemini-1.5-flash-latest">Gemini 2.5 Flash</option>
120
- <option value="gemini:gemini-1.5-pro-latest">Gemini 2.5 Pro</option>
121
- </optgroup>
122
- <optgroup label="Groq (Ultra RΓ‘pido)">
123
- <option value="groq:llama-3.1-8b-instant">Llama 3.1 8B</option>
124
- <option value="groq:llama-3.1-70b-versatile">Llama 3.1 70B</option>
125
- <option value="groq:openai/gpt-oss-120b">OpenAI GPT-OSS 120B</option>
126
- </optgroup>
127
- </select>
128
- </div>
129
- <div>
130
- <label for="contextSelector" class="block text-sm font-medium text-gray-700">Foco do VocabulΓ‘rio</label>
131
- <select id="contextSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
132
- <option value="General/Social">Geral/Social</option>
133
- <option value="Professional/Business">Profissional/NegΓ³cios</option>
134
- <option value="Technical/IT">TΓ©cnico/TI</option>
135
- </select>
136
- </div>
137
- </div>
138
- </div>
139
  </div>
140
 
141
- <!-- Coluna da Direita: Flashcards -->
142
  <div id="flashcardsPanel" class="card">
143
- <h2 class="text-xl font-semibold mb-4 text-gray-700">Sua SessΓ£o de Estudo</h2>
144
  <div id="flashcardList" class="space-y-6">
145
- <p id="noCardsMessage" class="text-gray-500 italic">Nenhum flashcard criado ainda.</p>
146
  </div>
147
  </div>
148
  </div>
149
  </div>
150
 
151
  <script>
152
- // --- ELEMENTOS GLOBAIS ---
 
 
153
  const textEditor = document.getElementById('textEditor');
154
- const charCount = document.getElementById('charCount');
155
  const createCardButton = document.getElementById('createCardButton');
156
  const playButton = document.getElementById('playButton');
157
- const playText = document.getElementById('playText');
158
- const playSpinner = document.getElementById('playSpinner');
159
  const audioPlayer = document.getElementById('audioPlayer');
160
  const flashcardList = document.getElementById('flashcardList');
161
  const noCardsMessage = document.getElementById('noCardsMessage');
@@ -165,37 +174,145 @@
165
  const chatDisplay = document.getElementById('chatDisplay');
166
  const chatInput = document.getElementById('chatInput');
167
  const micButton = document.getElementById('micButton');
168
- const modelSelector = document.getElementById('modelSelector');
169
- const contextSelector = document.getElementById('contextSelector');
170
  const imagePromptInput = document.getElementById('imagePromptInput');
171
  const generateImageBtn = document.getElementById('generateImageBtn');
172
  const imageResultContainer = document.getElementById('imageResultContainer');
173
  const activityTypeSelector = document.getElementById('activityTypeSelector');
174
  const generateActivityButton = document.getElementById('generateActivityButton');
175
- const activityOutput = document.getElementById('activityOutput');
176
- const translateFullTextButton = document.getElementById('translateFullTextButton');
 
 
177
 
178
  let chatHistory = [];
179
  let recognition;
180
  let isRecording = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
- // --- LΓ“GICA PRINCIPAL DO EDITOR DE TEXTO ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  textEditor.addEventListener('input', () => {
184
  const len = textEditor.innerText.length;
185
  charCount.textContent = `${len}/10000`;
186
  playButton.disabled = len === 0;
187
- translateFullTextButton.disabled = len === 0;
188
  });
189
  textEditor.addEventListener('mouseup', () => {
190
  createCardButton.disabled = !window.getSelection().toString().trim();
191
  });
192
-
193
- // --- LΓ“GICA DE ÁUDIO DO TEXTO PRINCIPAL ---
194
  async function handlePlay() {
195
- const text = textEditor.innerText;
196
  if (!text) return;
197
  playButton.disabled = true;
198
- playText.textContent = 'Gerando...';
199
  playSpinner.classList.remove('hidden');
200
  try {
201
  const audioBlob = await fetchAudioBlob(text);
@@ -205,21 +322,19 @@
205
  audioPlayer.play();
206
  audioPlayer.onended = () => URL.revokeObjectURL(audioUrl);
207
  } catch (error) {
208
- alert(`Erro ao gerar Γ‘udio: ${error.message}. Verifique o console para mais detalhes.`);
209
  } finally {
210
  playButton.disabled = false;
211
- playText.textContent = 'Ouvir Texto';
212
  playSpinner.classList.add('hidden');
213
  }
214
  }
215
-
216
- // --- LΓ“GICA DE CONVERSAÇÃO (VOZ E TEXTO) ---
217
  function initializeSpeechRecognition() {
218
- window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
219
  if (!window.SpeechRecognition) {
220
  micButton.disabled = true;
221
  micButton.textContent = '❌';
222
- alert('A API de reconhecimento de voz nΓ£o Γ© suportada neste navegador.');
223
  return;
224
  }
225
  recognition = new SpeechRecognition();
@@ -232,7 +347,7 @@
232
  chatInput.value = transcript;
233
  sendMessage(transcript);
234
  };
235
- recognition.onerror = (event) => { console.error('Erro no reconhecimento de voz:', event.error); };
236
  recognition.onend = () => {
237
  isRecording = false;
238
  micButton.classList.remove('is-recording');
@@ -271,7 +386,7 @@
271
  chatHistory.push({ role: 'assistant', content: data.response });
272
  playAudio(data.response);
273
  } catch (error) {
274
- appendMessage(`Erro na conversa: ${error.message}`, 'ai');
275
  }
276
  }
277
  function appendMessage(text, sender) {
@@ -281,8 +396,6 @@
281
  chatDisplay.appendChild(msg);
282
  chatDisplay.scrollTop = chatDisplay.scrollHeight;
283
  }
284
-
285
- // --- LΓ“GICA DE ESTUDO POR IMAGEM (ANÁLISE) ---
286
  imageUploader.addEventListener('change', (event) => {
287
  const file = event.target.files[0];
288
  if (!file) return;
@@ -295,22 +408,25 @@
295
  reader.readAsDataURL(file);
296
  });
297
  async function analyzeImage(base64Image) {
298
- 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>`;
299
  try {
300
  const response = await fetch('/analyze-image', {
301
  method: 'POST',
302
  headers: { 'Content-Type': 'application/json' },
303
- body: JSON.stringify({ image: base64Image })
 
 
 
304
  });
305
  if (!response.ok) throw new Error((await response.json()).error);
306
  const vocabulary = await response.json();
307
  displayImageVocabulary(vocabulary);
308
  } catch (error) {
309
- imageAnalysisResult.innerHTML = `<p class="text-red-600 font-semibold">Falha na anΓ‘lise: ${error.message}</p>`;
310
  }
311
  }
312
  function displayImageVocabulary(vocabulary) {
313
- imageAnalysisResult.innerHTML = '<h3 class="font-semibold mb-2">VocabulΓ‘rio Sugerido:</h3>';
314
  const buttonContainer = document.createElement('div');
315
  buttonContainer.className = 'flex flex-wrap gap-2';
316
  vocabulary.forEach(({ term, definition }) => {
@@ -322,17 +438,15 @@
322
  });
323
  imageAnalysisResult.appendChild(buttonContainer);
324
  }
325
-
326
- // --- LΓ“GICA DE GERAÇÃO DE IMAGEM (NANO BANANA) ---
327
  async function generateImage() {
328
  const prompt = imagePromptInput.value.trim();
329
  if (!prompt) {
330
- alert('Por favor, digite uma descriΓ§Γ£o para a imagem.');
331
  return;
332
  }
333
  generateImageBtn.disabled = true;
334
- generateImageBtn.innerHTML = '<div class="spinner mr-2"></div> Gerando...';
335
- 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>`;
336
  try {
337
  const response = await fetch('/generate-image', {
338
  method: 'POST',
@@ -341,99 +455,19 @@
341
  });
342
  if (!response.ok) throw new Error((await response.json()).error);
343
  const data = await response.json();
344
- imageResultContainer.innerHTML = `<img src="data:image/png;base64,${data.image_base64}" class="rounded-lg max-h-60 mx-auto" alt="Imagem gerada por IA"/>`;
345
  } catch (error) {
346
- imageResultContainer.innerHTML = `<p class="text-red-600 font-semibold">Falha ao gerar imagem: ${error.message}</p>`;
347
  } finally {
348
  generateImageBtn.disabled = false;
349
- generateImageBtn.textContent = 'Gerar';
350
  }
351
  }
352
-
353
- // --- LΓ“GICA DE ATIVIDADES DE ESCRITA E TRADUÇÃO ---
354
- async function generateActivity() {
355
- const activityType = activityTypeSelector.value;
356
- const context = contextSelector.value;
357
- const sourceText = textEditor.innerText;
358
-
359
- generateActivityButton.disabled = true;
360
- generateActivityButton.innerHTML = '<div class="spinner mr-2"></div> Gerando...';
361
- activityOutput.innerHTML = '<p class="text-gray-500 italic">Gerando seu exercΓ­cio...</p>';
362
-
363
- let prompt = "";
364
- if (activityType === 'translation_pt_en') {
365
- prompt = `Baseado no contexto '${context}', crie uma frase em portuguΓͺs para eu traduzir para o inglΓͺs.`;
366
- } else if (activityType === 'email_writing') {
367
- prompt = `Crie um cenΓ‘rio para eu escrever um e-mail formal, relacionado ao contexto '${context}'.`;
368
- } else if (activityType === 'summary_writing') {
369
- if (!sourceText) {
370
- alert("Por favor, cole um texto no editor para gerar a atividade de resumo.");
371
- activityOutput.innerHTML = '<p class="text-red-500">Cole um texto acima primeiro.</p>';
372
- generateActivityButton.disabled = false;
373
- generateActivityButton.textContent = 'Gerar Atividade';
374
- return;
375
- }
376
- prompt = `Resuma o seguinte texto em uma frase: "${sourceText}"`;
377
- }
378
-
379
- try {
380
- const response = await fetch('/explain-proxy', {
381
- method: 'POST',
382
- headers: { 'Content-Type': 'application/json' },
383
- body: JSON.stringify({
384
- model: modelSelector.value,
385
- context_focus: context,
386
- custom_prompt: prompt
387
- })
388
- });
389
- if (!response.ok) throw new Error((await response.json()).error);
390
- const data = await response.json();
391
- activityOutput.textContent = data.explanation;
392
- } catch (error) {
393
- activityOutput.innerHTML = `<p class="text-red-600 font-semibold">Falha ao gerar atividade: ${error.message}</p>`;
394
- } finally {
395
- generateActivityButton.disabled = false;
396
- generateActivityButton.textContent = 'Gerar Atividade';
397
- }
398
- }
399
-
400
- async function translateFullText() {
401
- const text = textEditor.innerText;
402
- if (!text) return;
403
-
404
- translateFullTextButton.disabled = true;
405
- translateFullTextButton.innerHTML = '<div class="spinner mr-2"></div> Traduzindo...';
406
-
407
- const prompt = `Traduza o seguinte texto para o portuguΓͺs de forma clara e natural: "${text}"`;
408
-
409
- try {
410
- const response = await fetch('/explain-proxy', {
411
- method: 'POST',
412
- headers: { 'Content-Type': 'application/json' },
413
- body: JSON.stringify({
414
- model: modelSelector.value,
415
- context_focus: contextSelector.value,
416
- custom_prompt: prompt
417
- })
418
- });
419
- if (!response.ok) throw new Error((await response.json()).error);
420
- const data = await response.json();
421
- alert("TraduΓ§Γ£o Completa:\n\n" + data.explanation);
422
- } catch (error) {
423
- alert(`Falha na traduΓ§Γ£o: ${error.message}`);
424
- } finally {
425
- translateFullTextButton.disabled = false;
426
- translateFullTextButton.textContent = 'Traduzir Texto Completo para PT';
427
- }
428
- }
429
-
430
-
431
- // --- LΓ“GICA DE GERAÇÃO DE FLASHCARD ---
432
  async function createFlashcard(word, context, definition = null) {
433
  const btn = definition ? null : createCardButton;
434
  if (btn) {
435
  btn.disabled = true;
436
- btn.innerHTML = '<div class="spinner mr-2"></div> Criando...';
437
  }
438
  try {
439
  const response = await fetch('/explain-proxy', {
@@ -449,17 +483,17 @@
449
  });
450
  if (!response.ok) {
451
  const errData = await response.json();
452
- throw new Error(errData.error || 'Erro desconhecido do servidor.');
453
  }
454
  const cardData = await response.json();
455
  if(definition && !cardData.definition) cardData.definition = definition;
456
  renderFlashcard(cardData);
457
  } catch (error) {
458
- alert(`Falha ao criar flashcard: ${error.message}. Verifique as chaves de API e a seleΓ§Γ£o do modelo.`);
459
  } finally {
460
  if (btn) {
461
  btn.disabled = false;
462
- btn.innerHTML = '+ Criar Flashcard';
463
  }
464
  }
465
  }
@@ -479,32 +513,32 @@
479
  const minHeight = 380;
480
  container.style.minHeight = `${minHeight}px`;
481
 
482
- const fallback = 'InformaΓ§Γ£o nΓ£o gerada';
483
 
484
  container.innerHTML = `
485
  <button class="delete-btn" onclick="deleteFlashcard('${cardId}', event)">πŸ—‘οΈ</button>
486
  <div class="flashcard-inner" style="min-height: ${minHeight}px;">
487
  <div class="flashcard-front">
488
  <div>
489
- <div class="text-sm text-gray-600 mb-2">Frase de Contexto:</div>
490
  <p class="text-lg text-center text-gray-800">${data.gapped_sentence || fallback}</p>
491
  </div>
492
  <div class="border-t pt-4 mt-4 text-center">
493
- <strong class="text-indigo-600">Dica (PT):</strong> ${data.translation || fallback}
494
  </div>
495
- <div class="text-xs text-center text-gray-400 mt-4">Clique para ver a resposta</div>
496
  </div>
497
  <div class="flashcard-back">
498
  <div>
499
- <h3 class="text-xl font-bold text-center text-indigo-600 mb-2">${data.term || 'Termo'}</h3>
500
  <p class="flex items-center justify-center gap-2 text-gray-600 italic mb-3">
501
  <button class="audio-btn" onclick="playAudio('${(data.context_sentence || '').replace(/'/g, "\\'")}', event)">πŸ”Š</button>
502
  <span>${data.context_sentence || fallback}</span>
503
  </p>
504
- <p class="text-sm text-gray-800"><strong class="font-semibold">DefiniΓ§Γ£o:</strong> ${data.definition || fallback}</p>
505
  </div>
506
  <div class="border-t mt-3 pt-3">
507
- <h4 class="text-sm font-semibold text-center">Pratique sua PronΓΊncia</h4>
508
  <div class="flex justify-center items-center gap-2 mt-2">
509
  <button class="btn btn-primary w-12 h-12 rounded-full" onclick="handlePronunciationPractice(this, '${(data.context_sentence || '').replace(/'/g, "\\'")}', event)">πŸŽ™οΈ</button>
510
  </div>
@@ -527,7 +561,7 @@
527
  if (cardToDelete) {
528
  cardToDelete.remove();
529
  }
530
- if (flashcardList.children.length === 1) {
531
  noCardsMessage.classList.remove('hidden');
532
  }
533
  }
@@ -541,13 +575,13 @@
541
 
542
  button.innerHTML = '<div class="spinner"></div>';
543
  button.disabled = true;
544
- feedbackArea.textContent = 'Ouvindo...';
545
 
546
  practiceRecognition.start();
547
 
548
  practiceRecognition.onresult = async (e) => {
549
  const userText = e.results[0][0].transcript;
550
- feedbackArea.textContent = `VocΓͺ disse: "${userText}". Analisando...`;
551
  try {
552
  const response = await fetch('/pronunciation-feedback', {
553
  method: 'POST',
@@ -558,20 +592,19 @@
558
  const data = await response.json();
559
  feedbackArea.innerHTML = `<strong>Feedback:</strong> ${data.feedback}`;
560
  } catch (error) {
561
- feedbackArea.textContent = `Erro na anΓ‘lise: ${error.message}`;
562
  } finally {
563
  button.innerHTML = 'πŸŽ™οΈ';
564
  button.disabled = false;
565
  }
566
  };
567
  practiceRecognition.onerror = (e) => {
568
- feedbackArea.textContent = `Erro ao gravar: ${e.error}`;
569
  button.innerHTML = 'πŸŽ™οΈ';
570
  button.disabled = false;
571
  };
572
  }
573
 
574
- // --- FUNÇÕES AUXILIARES DE ÁUDIO ---
575
  async function fetchAudioBlob(text) {
576
  const response = await fetch('/tts-proxy', {
577
  method: 'POST',
@@ -592,10 +625,10 @@
592
  const audio = new Audio(audioUrl);
593
  audio.play();
594
  audio.onended = () => URL.revokeObjectURL(audioUrl);
595
- } catch (error) { console.error('Erro ao tocar Γ‘udio:', error); }
596
  }
597
 
598
- // --- INICIALIZAÇÃO ---
599
  document.addEventListener('DOMContentLoaded', () => {
600
  initializeSpeechRecognition();
601
  });
 
1
  <!DOCTYPE html>
2
+ <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Dynamic English Study Studio</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
+ <!-- Including the Marked library to render Markdown -->
9
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
10
  <style>
11
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
12
  body { font-family: 'Inter', sans-serif; background-color: #f7f9fc; }
 
24
  .flashcard-front { background-color: #f9fafb; }
25
  .flashcard-back { background-color: #ffffff; transform: rotateY(180deg); }
26
  .audio-btn { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #4f46e5; padding: 0; }
 
 
 
 
 
27
  .delete-btn { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #9ca3af; z-index: 10; }
28
  .delete-btn:hover { color: #ef4444; }
29
+ /* Styles for the new activity section */
30
+ #activityChatDisplay { border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; min-height: 150px; background-color: #f9fafb; }
31
+ .ai-prompt { border-bottom: 2px dashed #d1d5db; padding-bottom: 1rem; margin-bottom: 1rem; }
32
+ .user-response-display { background-color: #eef2ff; border-left: 4px solid #4f46e5; padding: 0.75rem; margin-bottom: 1rem; font-style: italic; }
33
+ .ai-feedback { background-color: #f0fdf4; border-left: 4px solid #22c55e; padding: 0.75rem; }
34
+ .ai-feedback h3 { font-weight: 700; }
35
  </style>
36
  </head>
37
  <body class="p-4 md:p-8">
 
39
  <div class="container mx-auto max-w-5xl">
40
  <header class="text-center mb-10">
41
  <h1 class="text-3xl lg:text-4xl font-bold text-gray-800">Dynamic English Study Studio</h1>
42
+ <p class="text-gray-500 mt-2">Your study tool with audio, images, and conversation & pronunciation practice.</p>
43
  </header>
44
 
45
+ <!-- GLOBAL SETTINGS AT THE TOP -->
46
+ <div class="card mb-8">
47
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">Global Session Settings</h2>
48
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
49
+ <div>
50
+ <label for="modelSelector" class="block text-sm font-medium text-gray-700">Main AI Model</label>
51
+ <select id="modelSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
52
+ <optgroup label="Google Gemini">
53
+ <option value="gemini:gemini-2.5-flash-latest">Gemini 2.5 Flash</option>
54
+ <option value="gemini:gemini-2.5-pro-latest">Gemini 2.5 Pro</option>
55
+ </optgroup>
56
+ <optgroup label="Groq (Ultra Fast)">
57
+ <option value="groq:llama-3.1-8b-instant">Llama 3.1 8B</option>
58
+ <option value="groq:llama-3.1-70b-versatile">Llama 3.1 70B</option>
59
+ </optgroup>
60
+ </select>
61
+ </div>
62
+ <div>
63
+ <label for="contextSelector" class="block text-sm font-medium text-gray-700">Vocabulary Focus</label>
64
+ <select id="contextSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
65
+ <option value="General/Social">General/Social</option>
66
+ <option value="Professional/Business">Professional/Business</option>
67
+ <option value="Technical/IT">Technical/IT</option>
68
+ </select>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
74
+ <!-- Left Column: Inputs -->
75
  <div class="flex flex-col gap-6">
76
  <div class="card">
77
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">1. Study from Text</h2>
78
  <div id="textEditor" contenteditable="true" spellcheck="true" lang="en" class="w-full p-4 border rounded-md min-h-[150px]"></div>
79
+ <p class="text-sm text-right text-gray-500 mt-2">Characters: <span id="charCount">0</span>/10000</p>
80
  <div class="mt-4 border-t pt-4 flex flex-wrap gap-2 justify-between items-center">
81
+ <button id="createCardButton" onclick="createFlashcardFromSelection()" class="btn btn-primary bg-green-600 hover:bg-green-700 disabled:opacity-50" disabled>+ Create Flashcard</button>
82
  <button id="playButton" onclick="handlePlay()" class="btn btn-primary" disabled>
83
+ <span id="playText">Listen to Text</span>
84
  <div id="playSpinner" class="spinner ml-2 hidden"></div>
85
  </button>
86
  </div>
 
88
  </div>
89
 
90
  <div class="card">
91
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">2. Study from Image (Analysis)</h2>
92
  <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"/>
93
+ <img id="imagePreview" src="" class="mt-4 rounded-lg hidden max-h-60 mx-auto" alt="Preview"/>
94
  <div id="imageAnalysisResult" class="mt-4"></div>
95
  </div>
96
 
97
  <div class="card">
98
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">3. Conversation Practice</h2>
99
  <div id="chatDisplay" class="flex flex-col space-y-2 mb-4"></div>
100
  <div class="flex gap-2">
101
+ <input type="text" id="chatInput" class="flex-grow border rounded-lg px-3 py-2" placeholder="Type or use the microphone...">
102
+ <button onclick="handleTextInput()" class="btn btn-primary bg-gray-500 hover:bg-gray-600">Send</button>
103
  <button id="micButton" onclick="handleVoiceInput()" class="btn btn-primary mic-button w-12 h-12 rounded-full">πŸŽ™οΈ</button>
104
  </div>
105
  </div>
106
 
107
  <div class="card">
108
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">4. Image Generator (Nano Banana)</h2>
109
+ <p class="text-sm text-gray-600 mb-2">Describe a scene in English for the AI to draw.</p>
110
  <div class="flex gap-2">
111
  <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">
112
+ <button id="generateImageBtn" onclick="generateImage()" class="btn btn-primary">Generate</button>
113
  </div>
114
  <div id="imageResultContainer" class="mt-4 p-4 border rounded-lg bg-gray-50 min-h-[200px] flex items-center justify-center">
115
+ <p class="text-gray-500 italic">Your image will appear here.</p>
116
  </div>
117
  </div>
118
 
119
+ <!-- REDESIGNED ACTIVITY SECTION -->
120
  <div class="card">
121
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">5. Interactive Writing Practice</h2>
122
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
123
  <div>
124
+ <label for="activityTypeSelector" class="block text-sm font-medium text-gray-700">Activity Type</label>
125
  <select id="activityTypeSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
126
+ <option value="translate_pt_en">Translate (PT to EN)</option>
127
+ <option value="email_writing">Write Formal Email</option>
128
+ <option value="summary_writing">Summarize Main Text</option>
129
+ <option value="professional_conversation">Simulate Professional Conversation</option>
130
  </select>
131
  </div>
132
  <div class="flex items-end">
133
+ <button id="generateActivityButton" onclick="generateActivity()" class="btn btn-primary w-full">Generate Activity</button>
134
  </div>
135
  </div>
136
+
137
+ <div id="activityChatContainer" class="mt-4 border-t pt-4">
138
+ <div id="activityChatDisplay">
139
+ <p class="text-gray-500 italic">Your interactive exercise will appear here.</p>
140
+ </div>
141
+ <div id="activityResponseArea" class="mt-4 hidden">
142
+ <label for="activityInput" class="block text-sm font-medium text-gray-700 mb-1">Your Answer:</label>
143
+ <textarea id="activityInput" rows="4" class="w-full p-2 border rounded-md"></textarea>
144
+ <button id="submitActivityBtn" onclick="submitActivityResponse()" class="btn btn-primary mt-2">Submit Answer</button>
145
+ </div>
146
  </div>
147
  </div>
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </div>
150
 
151
+ <!-- Right Column: Flashcards -->
152
  <div id="flashcardsPanel" class="card">
153
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">Your Study Session</h2>
154
  <div id="flashcardList" class="space-y-6">
155
+ <p id="noCardsMessage" class="text-gray-500 italic">No flashcards created yet.</p>
156
  </div>
157
  </div>
158
  </div>
159
  </div>
160
 
161
  <script>
162
+ // --- GLOBAL ELEMENTS ---
163
+ const modelSelector = document.getElementById('modelSelector');
164
+ const contextSelector = document.getElementById('contextSelector');
165
  const textEditor = document.getElementById('textEditor');
 
166
  const createCardButton = document.getElementById('createCardButton');
167
  const playButton = document.getElementById('playButton');
 
 
168
  const audioPlayer = document.getElementById('audioPlayer');
169
  const flashcardList = document.getElementById('flashcardList');
170
  const noCardsMessage = document.getElementById('noCardsMessage');
 
174
  const chatDisplay = document.getElementById('chatDisplay');
175
  const chatInput = document.getElementById('chatInput');
176
  const micButton = document.getElementById('micButton');
 
 
177
  const imagePromptInput = document.getElementById('imagePromptInput');
178
  const generateImageBtn = document.getElementById('generateImageBtn');
179
  const imageResultContainer = document.getElementById('imageResultContainer');
180
  const activityTypeSelector = document.getElementById('activityTypeSelector');
181
  const generateActivityButton = document.getElementById('generateActivityButton');
182
+ const activityChatDisplay = document.getElementById('activityChatDisplay');
183
+ const activityResponseArea = document.getElementById('activityResponseArea');
184
+ const activityInput = document.getElementById('activityInput');
185
+ const submitActivityBtn = document.getElementById('submitActivityBtn');
186
 
187
  let chatHistory = [];
188
  let recognition;
189
  let isRecording = false;
190
+ let currentActivityPrompt = ""; // Stores the current activity prompt
191
+
192
+ // --- INTERACTIVE WRITING PRACTICE LOGIC (UPDATED) ---
193
+ async function generateActivity() {
194
+ const activityType = activityTypeSelector.value;
195
+ const context = contextSelector.value;
196
+ const sourceText = textEditor.innerText;
197
+
198
+ generateActivityButton.disabled = true;
199
+ generateActivityButton.innerHTML = '<div class="spinner mr-2"></div> Generating...';
200
+ activityChatDisplay.innerHTML = '<p class="text-gray-500 italic">Generating your exercise...</p>';
201
+ activityResponseArea.classList.add('hidden');
202
+
203
+ let prompt = "";
204
+ if (activityType === 'translate_pt_en') {
205
+ prompt = `Based on the '${context}' context, create one single sentence in Portuguese for me to translate into English.`;
206
+ } else if (activityType === 'email_writing') {
207
+ prompt = `Create a scenario for me to write a formal email, related to the '${context}' context. Give me clear instructions on what to write.`;
208
+ } else if (activityType === 'summary_writing') {
209
+ if (!sourceText) {
210
+ alert("Please paste text in the main editor to generate a summary activity.");
211
+ activityChatDisplay.innerHTML = '<p class="text-red-500">Please paste text in the editor in card #1 first.</p>';
212
+ generateActivityButton.disabled = false;
213
+ generateActivityButton.textContent = 'Generate Activity';
214
+ return;
215
+ }
216
+ prompt = `Your task is to summarize the following text in 2 or 3 sentences: "${sourceText}"`;
217
+ } else if (activityType === 'professional_conversation') {
218
+ prompt = `Start a professional role-playing conversation with me related to '${context}'. Ask me an open-ended question to begin.`;
219
+ }
220
+
221
+ try {
222
+ const response = await fetch('/explain-proxy', {
223
+ method: 'POST',
224
+ headers: { 'Content-Type': 'application/json' },
225
+ body: JSON.stringify({
226
+ model: modelSelector.value,
227
+ context_focus: context,
228
+ custom_prompt: prompt
229
+ })
230
+ });
231
+ if (!response.ok) throw new Error((await response.json()).error);
232
+ const data = await response.json();
233
+ currentActivityPrompt = data.explanation; // Saves the prompt
234
+
235
+ activityChatDisplay.innerHTML = `<div class="ai-prompt"><strong>Your Task:</strong><p>${currentActivityPrompt}</p></div>`;
236
+ activityResponseArea.classList.remove('hidden'); // Shows the answer area
237
+ } catch (error) {
238
+ activityChatDisplay.innerHTML = `<p class="text-red-600 font-semibold">Failed to generate activity: ${error.message}</p>`;
239
+ } finally {
240
+ generateActivityButton.disabled = false;
241
+ generateActivityButton.textContent = 'Generate Activity';
242
+ }
243
+ }
244
+
245
+ async function submitActivityResponse() {
246
+ const userResponse = activityInput.value.trim();
247
+ if (!userResponse) {
248
+ alert("Please write a response.");
249
+ return;
250
+ }
251
+
252
+ submitActivityBtn.disabled = true;
253
+ submitActivityBtn.innerHTML = '<div class="spinner mr-2"></div> Getting Feedback...';
254
+
255
+ // Removes any old feedback
256
+ const oldFeedback = activityChatDisplay.querySelector('.ai-feedback');
257
+ if(oldFeedback) oldFeedback.remove();
258
+ const oldResponse = activityChatDisplay.querySelector('.user-response-display');
259
+ if(oldResponse) oldResponse.remove();
260
+
261
+
262
+ // Adds the user's response to the screen
263
+ const userResponseDiv = document.createElement('div');
264
+ userResponseDiv.className = 'user-response-display';
265
+ userResponseDiv.innerHTML = `<strong>Your Response:</strong><p>${userResponse}</p>`;
266
+ activityChatDisplay.appendChild(userResponseDiv);
267
+
268
+ try {
269
+ const response = await fetch('/activity-feedback', {
270
+ method: 'POST',
271
+ headers: { 'Content-Type': 'application/json' },
272
+ body: JSON.stringify({
273
+ model: modelSelector.value,
274
+ context_focus: contextSelector.value,
275
+ original_prompt: currentActivityPrompt,
276
+ user_response: userResponse
277
+ })
278
+ });
279
+ if (!response.ok) throw new Error((await response.json()).error);
280
+ const data = await response.json();
281
 
282
+ const feedbackDiv = document.createElement('div');
283
+ feedbackDiv.className = 'ai-feedback';
284
+ // Uses the Marked library to render Markdown from the AI's response
285
+ feedbackDiv.innerHTML = `<strong>Feedback:</strong><div class="prose prose-sm max-w-none">${marked.parse(data.feedback)}</div>`;
286
+ activityChatDisplay.appendChild(feedbackDiv);
287
+ activityInput.value = ''; // Clears the text field
288
+ } catch (error) {
289
+ const errorDiv = document.createElement('div');
290
+ errorDiv.className = 'ai-feedback';
291
+ errorDiv.innerHTML = `<p class="text-red-600 font-semibold">Failed to get feedback: ${error.message}</p>`;
292
+ activityChatDisplay.appendChild(errorDiv);
293
+ } finally {
294
+ submitActivityBtn.disabled = false;
295
+ submitActivityBtn.textContent = 'Submit Answer';
296
+ }
297
+ }
298
+
299
+ // --- OTHER FUNCTIONS (no changes) ---
300
+ const charCount = document.getElementById('charCount');
301
+ const playText = document.getElementById('playText');
302
+ const playSpinner = document.getElementById('playSpinner');
303
  textEditor.addEventListener('input', () => {
304
  const len = textEditor.innerText.length;
305
  charCount.textContent = `${len}/10000`;
306
  playButton.disabled = len === 0;
 
307
  });
308
  textEditor.addEventListener('mouseup', () => {
309
  createCardButton.disabled = !window.getSelection().toString().trim();
310
  });
 
 
311
  async function handlePlay() {
312
+ const text = textEditor.innerText;
313
  if (!text) return;
314
  playButton.disabled = true;
315
+ playText.textContent = 'Generating...';
316
  playSpinner.classList.remove('hidden');
317
  try {
318
  const audioBlob = await fetchAudioBlob(text);
 
322
  audioPlayer.play();
323
  audioPlayer.onended = () => URL.revokeObjectURL(audioUrl);
324
  } catch (error) {
325
+ alert(`Error generating audio: ${error.message}. Check the console for more details.`);
326
  } finally {
327
  playButton.disabled = false;
328
+ playText.textContent = 'Listen to Text';
329
  playSpinner.classList.add('hidden');
330
  }
331
  }
 
 
332
  function initializeSpeechRecognition() {
333
+ window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
334
  if (!window.SpeechRecognition) {
335
  micButton.disabled = true;
336
  micButton.textContent = '❌';
337
+ alert('Voice recognition API is not supported in this browser.');
338
  return;
339
  }
340
  recognition = new SpeechRecognition();
 
347
  chatInput.value = transcript;
348
  sendMessage(transcript);
349
  };
350
+ recognition.onerror = (event) => { console.error('Voice recognition error:', event.error); };
351
  recognition.onend = () => {
352
  isRecording = false;
353
  micButton.classList.remove('is-recording');
 
386
  chatHistory.push({ role: 'assistant', content: data.response });
387
  playAudio(data.response);
388
  } catch (error) {
389
+ appendMessage(`Chat error: ${error.message}`, 'ai');
390
  }
391
  }
392
  function appendMessage(text, sender) {
 
396
  chatDisplay.appendChild(msg);
397
  chatDisplay.scrollTop = chatDisplay.scrollHeight;
398
  }
 
 
399
  imageUploader.addEventListener('change', (event) => {
400
  const file = event.target.files[0];
401
  if (!file) return;
 
408
  reader.readAsDataURL(file);
409
  });
410
  async function analyzeImage(base64Image) {
411
+ 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>`;
412
  try {
413
  const response = await fetch('/analyze-image', {
414
  method: 'POST',
415
  headers: { 'Content-Type': 'application/json' },
416
+ body: JSON.stringify({
417
+ image: base64Image,
418
+ model: modelSelector.value
419
+ })
420
  });
421
  if (!response.ok) throw new Error((await response.json()).error);
422
  const vocabulary = await response.json();
423
  displayImageVocabulary(vocabulary);
424
  } catch (error) {
425
+ imageAnalysisResult.innerHTML = `<p class="text-red-600 font-semibold">Analysis failed: ${error.message}</p>`;
426
  }
427
  }
428
  function displayImageVocabulary(vocabulary) {
429
+ imageAnalysisResult.innerHTML = '<h3 class="font-semibold mb-2">Suggested Vocabulary:</h3>';
430
  const buttonContainer = document.createElement('div');
431
  buttonContainer.className = 'flex flex-wrap gap-2';
432
  vocabulary.forEach(({ term, definition }) => {
 
438
  });
439
  imageAnalysisResult.appendChild(buttonContainer);
440
  }
 
 
441
  async function generateImage() {
442
  const prompt = imagePromptInput.value.trim();
443
  if (!prompt) {
444
+ alert('Please enter a description for the image.');
445
  return;
446
  }
447
  generateImageBtn.disabled = true;
448
+ generateImageBtn.innerHTML = '<div class="spinner mr-2"></div> Generating...';
449
+ 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>`;
450
  try {
451
  const response = await fetch('/generate-image', {
452
  method: 'POST',
 
455
  });
456
  if (!response.ok) throw new Error((await response.json()).error);
457
  const data = await response.json();
458
+ imageResultContainer.innerHTML = `<img src="data:image/png;base64,${data.image_base64}" class="rounded-lg max-h-60 mx-auto" alt="AI-generated image"/>`;
459
  } catch (error) {
460
+ imageResultContainer.innerHTML = `<p class="text-red-600 font-semibold">Failed to generate image: ${error.message}</p>`;
461
  } finally {
462
  generateImageBtn.disabled = false;
463
+ generateImageBtn.textContent = 'Generate';
464
  }
465
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  async function createFlashcard(word, context, definition = null) {
467
  const btn = definition ? null : createCardButton;
468
  if (btn) {
469
  btn.disabled = true;
470
+ btn.innerHTML = '<div class="spinner mr-2"></div> Creating...';
471
  }
472
  try {
473
  const response = await fetch('/explain-proxy', {
 
483
  });
484
  if (!response.ok) {
485
  const errData = await response.json();
486
+ throw new Error(errData.error || 'Unknown server error.');
487
  }
488
  const cardData = await response.json();
489
  if(definition && !cardData.definition) cardData.definition = definition;
490
  renderFlashcard(cardData);
491
  } catch (error) {
492
+ alert(`Failed to create flashcard: ${error.message}. Check API keys and model selection.`);
493
  } finally {
494
  if (btn) {
495
  btn.disabled = false;
496
+ btn.innerHTML = '+ Create Flashcard';
497
  }
498
  }
499
  }
 
513
  const minHeight = 380;
514
  container.style.minHeight = `${minHeight}px`;
515
 
516
+ const fallback = 'Info not generated';
517
 
518
  container.innerHTML = `
519
  <button class="delete-btn" onclick="deleteFlashcard('${cardId}', event)">πŸ—‘οΈ</button>
520
  <div class="flashcard-inner" style="min-height: ${minHeight}px;">
521
  <div class="flashcard-front">
522
  <div>
523
+ <div class="text-sm text-gray-600 mb-2">Context Sentence:</div>
524
  <p class="text-lg text-center text-gray-800">${data.gapped_sentence || fallback}</p>
525
  </div>
526
  <div class="border-t pt-4 mt-4 text-center">
527
+ <strong class="text-indigo-600">Hint (PT):</strong> ${data.translation || fallback}
528
  </div>
529
+ <div class="text-xs text-center text-gray-400 mt-4">Click to see the answer</div>
530
  </div>
531
  <div class="flashcard-back">
532
  <div>
533
+ <h3 class="text-xl font-bold text-center text-indigo-600 mb-2">${data.term || 'Term'}</h3>
534
  <p class="flex items-center justify-center gap-2 text-gray-600 italic mb-3">
535
  <button class="audio-btn" onclick="playAudio('${(data.context_sentence || '').replace(/'/g, "\\'")}', event)">πŸ”Š</button>
536
  <span>${data.context_sentence || fallback}</span>
537
  </p>
538
+ <p class="text-sm text-gray-800"><strong class="font-semibold">Definition:</strong> ${data.definition || fallback}</p>
539
  </div>
540
  <div class="border-t mt-3 pt-3">
541
+ <h4 class="text-sm font-semibold text-center">Practice Your Pronunciation</h4>
542
  <div class="flex justify-center items-center gap-2 mt-2">
543
  <button class="btn btn-primary w-12 h-12 rounded-full" onclick="handlePronunciationPractice(this, '${(data.context_sentence || '').replace(/'/g, "\\'")}', event)">πŸŽ™οΈ</button>
544
  </div>
 
561
  if (cardToDelete) {
562
  cardToDelete.remove();
563
  }
564
+ if (flashcardList.children.length === 1) { // 1 because the <p> tag is still there
565
  noCardsMessage.classList.remove('hidden');
566
  }
567
  }
 
575
 
576
  button.innerHTML = '<div class="spinner"></div>';
577
  button.disabled = true;
578
+ feedbackArea.textContent = 'Listening...';
579
 
580
  practiceRecognition.start();
581
 
582
  practiceRecognition.onresult = async (e) => {
583
  const userText = e.results[0][0].transcript;
584
+ feedbackArea.textContent = `You said: "${userText}". Analyzing...`;
585
  try {
586
  const response = await fetch('/pronunciation-feedback', {
587
  method: 'POST',
 
592
  const data = await response.json();
593
  feedbackArea.innerHTML = `<strong>Feedback:</strong> ${data.feedback}`;
594
  } catch (error) {
595
+ feedbackArea.textContent = `Analysis error: ${error.message}`;
596
  } finally {
597
  button.innerHTML = 'πŸŽ™οΈ';
598
  button.disabled = false;
599
  }
600
  };
601
  practiceRecognition.onerror = (e) => {
602
+ feedbackArea.textContent = `Recording error: ${e.error}`;
603
  button.innerHTML = 'πŸŽ™οΈ';
604
  button.disabled = false;
605
  };
606
  }
607
 
 
608
  async function fetchAudioBlob(text) {
609
  const response = await fetch('/tts-proxy', {
610
  method: 'POST',
 
625
  const audio = new Audio(audioUrl);
626
  audio.play();
627
  audio.onended = () => URL.revokeObjectURL(audioUrl);
628
+ } catch (error) { console.error('Error playing audio:', error); }
629
  }
630
 
631
+ // --- INITIALIZATION ---
632
  document.addEventListener('DOMContentLoaded', () => {
633
  initializeSpeechRecognition();
634
  });