amauricunha commited on
Commit
c3c7b3b
·
verified ·
1 Parent(s): c7c44dd

Delete index.html

Browse files
Files changed (1) hide show
  1. index.html +0 -652
index.html DELETED
@@ -1,652 +0,0 @@
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
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
- <style>
10
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
11
- body { font-family: 'Inter', sans-serif; background-color: #f7f9fc; }
12
- .card { background-color: white; border-radius: 12px; box-shadow: 0 4px_12px rgba(0,0,0,0.08); padding: 24px; }
13
- .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; }
14
- .btn:disabled { cursor: not-allowed; opacity: 0.5; }
15
- .btn-primary { background-color: #4f46e5; color: white; }
16
- .btn-primary:hover:not(:disabled) { background-color: #4338ca; }
17
- .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; }
18
- @keyframes spin { to { transform: rotate(360deg); } }
19
- .flashcard-container { perspective: 1000px; position: relative; }
20
- .flashcard-inner { position: relative; width: 100%; transition: transform 0.6s; transform-style: preserve-3d; }
21
- .flashcard-container.flipped .flashcard-inner { transform: rotateY(180deg); }
22
- .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; }
23
- .flashcard-front { background-color: #f9fafb; }
24
- .flashcard-back { background-color: #ffffff; transform: rotateY(180deg); }
25
- .audio-btn { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #4f46e5; padding: 0; }
26
- .delete-btn { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #9ca3af; z-index: 10; }
27
- .delete-btn:hover { color: #ef4444; }
28
- #activityChatDisplay { border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; min-height: 150px; background-color: #f9fafb; }
29
- .ai-prompt { border-bottom: 2px dashed #d1d5db; padding-bottom: 1rem; margin-bottom: 1rem; }
30
- .user-response-display { background-color: #eef2ff; border-left: 4px solid #4f46e5; padding: 0.75rem; margin-bottom: 1rem; font-style: italic; }
31
- .ai-feedback { background-color: #f0fdf4; border-left: 4px solid #22c55e; padding: 0.75rem; }
32
- .ai-feedback h3 { font-weight: 700; }
33
- </style>
34
- </head>
35
- <body class="p-4 md:p-8">
36
-
37
- <div class="container mx-auto max-w-5xl">
38
- <header class="text-center mb-10">
39
- <h1 class="text-3xl lg:text-4xl font-bold text-gray-800">Dynamic English Study Studio</h1>
40
- <p class="text-gray-500 mt-2">Your study tool with audio, images, and conversation & pronunciation practice.</p>
41
- </header>
42
-
43
- <div class="card mb-8">
44
- <h2 class="text-xl font-semibold mb-4 text-gray-700">Global Session Settings</h2>
45
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
46
- <div>
47
- <label for="modelSelector" class="block text-sm font-medium text-gray-700">Main AI Model</label>
48
- <select id="modelSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
49
- <option>Loading models...</option>
50
- </select>
51
- </div>
52
- <div>
53
- <label for="contextSelector" class="block text-sm font-medium text-gray-700">Vocabulary Focus</label>
54
- <select id="contextSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
55
- <option value="General/Social">General/Social</option>
56
- <option value="Professional/Business">Professional/Business</option>
57
- <option value="Technical/IT">Technical/IT</option>
58
- </select>
59
- </div>
60
- <div>
61
- <label for="voiceSelector" class="block text-sm font-medium text-gray-700">Voice Accent (TTS)</label>
62
- <select id="voiceSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
63
- <option value="co.uk">British (Female)</option>
64
- <option value="com">American (Female)</option>
65
- <option value="com.au">Australian (Female)</option>
66
- </select>
67
- </div>
68
- </div>
69
- </div>
70
-
71
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
72
- <!-- Coluna da Esquerda: Inputs -->
73
- <div class="flex flex-col gap-6">
74
- <div class="card">
75
- <h2 class="text-xl font-semibold mb-4 text-gray-700">1. Study from Text</h2>
76
- <div id="textEditor" contenteditable="true" spellcheck="true" lang="en" class="w-full p-4 border rounded-md min-h-[150px]"></div>
77
- <p class="text-sm text-right text-gray-500 mt-2">Characters: <span id="charCount">0</span>/10000</p>
78
- <div class="mt-4 border-t pt-4 flex flex-wrap gap-2 justify-between items-center">
79
- <button id="createCardButton" onclick="createFlashcardFromSelection()" class="btn btn-primary bg-green-600 hover:bg-green-700 disabled:opacity-50" disabled>+ Create Flashcard</button>
80
- <button id="playButton" onclick="handlePlay()" class="btn btn-primary" disabled>
81
- <span id="playText">Listen to Text</span>
82
- <div id="playSpinner" class="spinner ml-2 hidden"></div>
83
- </button>
84
- </div>
85
- <audio id="audioPlayer" controls class="w-full mt-4 hidden"></audio>
86
- </div>
87
-
88
- <div class="card">
89
- <h2 class="text-xl font-semibold mb-4 text-gray-700">2. Study from Image (Analysis)</h2>
90
- <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"/>
91
- <img id="imagePreview" src="" class="mt-4 rounded-lg hidden max-h-60 mx-auto" alt="Preview"/>
92
- <div id="imageAnalysisResult" class="mt-4"></div>
93
- </div>
94
-
95
- <div class="card">
96
- <h2 class="text-xl font-semibold mb-4 text-gray-700">3. Conversation Practice</h2>
97
- <div id="chatDisplay" class="flex flex-col space-y-2 mb-4"></div>
98
- <div class="flex gap-2">
99
- <input type="text" id="chatInput" class="flex-grow border rounded-lg px-3 py-2" placeholder="Type or use the microphone...">
100
- <button onclick="handleTextInput()" class="btn btn-primary bg-gray-500 hover:bg-gray-600">Send</button>
101
- <button id="micButton" onclick="handleVoiceInput()" class="btn btn-primary mic-button w-12 h-12 rounded-full">🎙️</button>
102
- </div>
103
- </div>
104
-
105
- <div class="card">
106
- <h2 class="text-xl font-semibold mb-4 text-gray-700">4. Image Generator (Nano Banana)</h2>
107
- <p class="text-sm text-gray-600 mb-2">Describe a scene in English for the AI to draw.</p>
108
- <div class="flex gap-2">
109
- <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">
110
- <button id="generateImageBtn" onclick="generateImage()" class="btn btn-primary">Generate</button>
111
- </div>
112
- <div id="imageResultContainer" class="mt-4 p-4 border rounded-lg bg-gray-50 min-h-[200px] flex items-center justify-center">
113
- <p class="text-gray-500 italic">Your image will appear here.</p>
114
- </div>
115
- </div>
116
-
117
- <div class="card">
118
- <h2 class="text-xl font-semibold mb-4 text-gray-700">5. Interactive Writing Practice</h2>
119
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
120
- <div>
121
- <label for="activityTypeSelector" class="block text-sm font-medium text-gray-700">Activity Type</label>
122
- <select id="activityTypeSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
123
- <option value="translate_pt_en">Translate (PT to EN)</option>
124
- <option value="email_writing">Write Formal Email</option>
125
- <option value="summary_writing">Summarize Main Text</option>
126
- <option value="professional_conversation">Simulate Professional Conversation</option>
127
- </select>
128
- </div>
129
- <div class="flex items-end">
130
- <button id="generateActivityButton" onclick="generateActivity()" class="btn btn-primary w-full">Generate Activity</button>
131
- </div>
132
- </div>
133
-
134
- <div id="activityChatContainer" class="mt-4 border-t pt-4">
135
- <div id="activityChatDisplay">
136
- <p class="text-gray-500 italic">Your interactive exercise will appear here.</p>
137
- </div>
138
- <div id="activityResponseArea" class="mt-4 hidden">
139
- <label for="activityInput" class="block text-sm font-medium text-gray-700 mb-1">Your Answer:</label>
140
- <textarea id="activityInput" rows="4" class="w-full p-2 border rounded-md"></textarea>
141
- <button id="submitActivityBtn" onclick="submitActivityResponse()" class="btn btn-primary mt-2">Submit Answer</button>
142
- </div>
143
- </div>
144
- </div>
145
- </div>
146
-
147
- <!-- Coluna da Direita: Flashcards -->
148
- <div id="flashcardsPanel" class="card">
149
- <h2 class="text-xl font-semibold mb-4 text-gray-700">Your Study Session</h2>
150
- <div id="flashcardList" class="space-y-6">
151
- <p id="noCardsMessage" class="text-gray-500 italic">No flashcards created yet.</p>
152
- </div>
153
- </div>
154
- </div>
155
- </div>
156
-
157
- <script>
158
- // --- ELEMENTOS GLOBAIS ---
159
- const modelSelector = document.getElementById('modelSelector');
160
- const contextSelector = document.getElementById('contextSelector');
161
- const voiceSelector = document.getElementById('voiceSelector');
162
- const textEditor = document.getElementById('textEditor');
163
- const createCardButton = document.getElementById('createCardButton');
164
- const playButton = document.getElementById('playButton');
165
- const audioPlayer = document.getElementById('audioPlayer');
166
- const flashcardList = document.getElementById('flashcardList');
167
- const noCardsMessage = document.getElementById('noCardsMessage');
168
- const imageUploader = document.getElementById('imageUploader');
169
- const imagePreview = document.getElementById('imagePreview');
170
- const imageAnalysisResult = document.getElementById('imageAnalysisResult');
171
- const chatDisplay = document.getElementById('chatDisplay');
172
- const chatInput = document.getElementById('chatInput');
173
- const micButton = document.getElementById('micButton');
174
- const imagePromptInput = document.getElementById('imagePromptInput');
175
- const generateImageBtn = document.getElementById('generateImageBtn');
176
- const imageResultContainer = document.getElementById('imageResultContainer');
177
- const activityTypeSelector = document.getElementById('activityTypeSelector');
178
- const generateActivityButton = document.getElementById('generateActivityButton');
179
- const activityChatDisplay = document.getElementById('activityChatDisplay');
180
- const activityResponseArea = document.getElementById('activityResponseArea');
181
- const activityInput = document.getElementById('activityInput');
182
- const submitActivityBtn = document.getElementById('submitActivityBtn');
183
-
184
- let chatHistory = [];
185
- let recognition;
186
- let isRecording = false;
187
- let currentActivityPrompt = "";
188
-
189
- // --- CARREGAMENTO DINÂMICO DE MODELOS ---
190
- async function populateModelSelector() {
191
- try {
192
- const response = await fetch('/list-models');
193
- if (!response.ok) throw new Error('Failed to fetch model list.');
194
- const models = await response.json();
195
- modelSelector.innerHTML = '';
196
- const geminiGroup = document.createElement('optgroup');
197
- geminiGroup.label = 'Google Gemini';
198
- const groqGroup = document.createElement('optgroup');
199
- groqGroup.label = 'Groq (Ultra Fast)';
200
- models.forEach(model => {
201
- const option = document.createElement('option');
202
- option.value = model.value;
203
- option.textContent = model.name;
204
- if (model.value.startsWith('gemini:')) {
205
- geminiGroup.appendChild(option);
206
- } else if (model.value.startsWith('groq:')) {
207
- groqGroup.appendChild(option);
208
- }
209
- });
210
- modelSelector.appendChild(geminiGroup);
211
- modelSelector.appendChild(groqGroup);
212
- } catch (error) {
213
- console.error("Error populating models:", error);
214
- modelSelector.innerHTML = '<option>Error loading models</option>';
215
- }
216
- }
217
-
218
- // --- FUNÇÕES DE ÁUDIO ---
219
- async function fetchAudioBlob(text) {
220
- const selectedTld = voiceSelector.value;
221
- const response = await fetch('/tts-proxy', {
222
- method: 'POST',
223
- headers: { 'Content-Type': 'application/json' },
224
- body: JSON.stringify({ text: text, tld: selectedTld })
225
- });
226
- if (!response.ok) {
227
- const err = await response.json();
228
- throw new Error(err.error || `HTTP error! status: ${response.status}`);
229
- }
230
- return await response.blob();
231
- }
232
- async function playAudio(text, event) {
233
- if(event) event.stopPropagation();
234
- try {
235
- const audioBlob = await fetchAudioBlob(text);
236
- const audioUrl = URL.createObjectURL(audioBlob);
237
- const audio = new Audio(audioUrl);
238
- audio.play();
239
- audio.onended = () => URL.revokeObjectURL(audioUrl);
240
- } catch (error) { console.error('Error playing audio:', error); }
241
- }
242
-
243
- // --- LÓGICA DO EDITOR ---
244
- const charCount = document.getElementById('charCount');
245
- const playText = document.getElementById('playText');
246
- const playSpinner = document.getElementById('playSpinner');
247
- textEditor.addEventListener('input', () => {
248
- const len = textEditor.innerText.length;
249
- charCount.textContent = `${len}/10000`;
250
- playButton.disabled = len === 0;
251
- });
252
- textEditor.addEventListener('mouseup', () => {
253
- createCardButton.disabled = !window.getSelection().toString().trim();
254
- });
255
- async function handlePlay() {
256
- const text = textEditor.innerText;
257
- if (!text) return;
258
- playButton.disabled = true;
259
- playText.textContent = 'Generating...';
260
- playSpinner.classList.remove('hidden');
261
- try {
262
- const audioBlob = await fetchAudioBlob(text);
263
- const audioUrl = URL.createObjectURL(audioBlob);
264
- audioPlayer.src = audioUrl;
265
- audioPlayer.classList.remove('hidden');
266
- audioPlayer.play();
267
- audioPlayer.onended = () => URL.revokeObjectURL(audioUrl);
268
- } catch (error) {
269
- alert(`Error generating audio: ${error.message}. Check the console for more details.`);
270
- } finally {
271
- playButton.disabled = false;
272
- playText.textContent = 'Listen to Text';
273
- playSpinner.classList.add('hidden');
274
- }
275
- }
276
-
277
- // --- TODAS AS OUTRAS FUNÇÕES ---
278
-
279
- // ATIVIDADES
280
- async function generateActivity() {
281
- const activityType = activityTypeSelector.value;
282
- const context = contextSelector.value;
283
- const sourceText = textEditor.innerText;
284
- generateActivityButton.disabled = true;
285
- generateActivityButton.innerHTML = '<div class="spinner mr-2"></div> Generating...';
286
- activityChatDisplay.innerHTML = '<p class="text-gray-500 italic">Generating your exercise...</p>';
287
- activityResponseArea.classList.add('hidden');
288
- let prompt = "";
289
- if (activityType === 'translate_pt_en') {
290
- prompt = `Based on the '${context}' context, create one single sentence in Portuguese for me to translate into English.`;
291
- } else if (activityType === 'email_writing') {
292
- prompt = `Create a scenario for me to write a formal email, related to the '${context}' context. Give me clear instructions on what to write.`;
293
- } else if (activityType === 'summary_writing') {
294
- if (!sourceText) {
295
- alert("Please paste text in the main editor to generate a summary activity.");
296
- activityChatDisplay.innerHTML = '<p class="text-red-500">Please paste text in the editor in card #1 first.</p>';
297
- generateActivityButton.disabled = false;
298
- generateActivityButton.textContent = 'Generate Activity';
299
- return;
300
- }
301
- prompt = `Your task is to summarize the following text in 2 or 3 sentences: "${sourceText}"`;
302
- } else if (activityType === 'professional_conversation') {
303
- prompt = `Start a professional role-playing conversation with me related to '${context}'. Ask me an open-ended question to begin.`;
304
- }
305
- try {
306
- const response = await fetch('/explain-proxy', {
307
- method: 'POST',
308
- headers: { 'Content-Type': 'application/json' },
309
- body: JSON.stringify({
310
- model: modelSelector.value,
311
- context_focus: context,
312
- custom_prompt: prompt
313
- })
314
- });
315
- if (!response.ok) throw new Error((await response.json()).error);
316
- const data = await response.json();
317
- currentActivityPrompt = data.explanation;
318
- activityChatDisplay.innerHTML = `<div class="ai-prompt"><strong>Your Task:</strong><p>${currentActivityPrompt}</p></div>`;
319
- activityResponseArea.classList.remove('hidden');
320
- } catch (error) {
321
- activityChatDisplay.innerHTML = `<p class="text-red-600 font-semibold">Failed to generate activity: ${error.message}</p>`;
322
- } finally {
323
- generateActivityButton.disabled = false;
324
- generateActivityButton.textContent = 'Generate Activity';
325
- }
326
- }
327
- async function submitActivityResponse() {
328
- const userResponse = activityInput.value.trim();
329
- if (!userResponse) {
330
- alert("Please write a response.");
331
- return;
332
- }
333
- submitActivityBtn.disabled = true;
334
- submitActivityBtn.innerHTML = '<div class="spinner mr-2"></div> Getting Feedback...';
335
- const oldFeedback = activityChatDisplay.querySelector('.ai-feedback');
336
- if(oldFeedback) oldFeedback.remove();
337
- const oldResponse = activityChatDisplay.querySelector('.user-response-display');
338
- if(oldResponse) oldResponse.remove();
339
- const userResponseDiv = document.createElement('div');
340
- userResponseDiv.className = 'user-response-display';
341
- userResponseDiv.innerHTML = `<strong>Your Response:</strong><p>${userResponse}</p>`;
342
- activityChatDisplay.appendChild(userResponseDiv);
343
- try {
344
- const response = await fetch('/activity-feedback', {
345
- method: 'POST',
346
- headers: { 'Content-Type': 'application/json' },
347
- body: JSON.stringify({
348
- model: modelSelector.value,
349
- context_focus: contextSelector.value,
350
- original_prompt: currentActivityPrompt,
351
- user_response: userResponse
352
- })
353
- });
354
- if (!response.ok) throw new Error((await response.json()).error);
355
- const data = await response.json();
356
- const feedbackDiv = document.createElement('div');
357
- feedbackDiv.className = 'ai-feedback';
358
- feedbackDiv.innerHTML = `<strong>Feedback:</strong><div class="prose prose-sm max-w-none">${marked.parse(data.feedback)}</div>`;
359
- activityChatDisplay.appendChild(feedbackDiv);
360
- activityInput.value = '';
361
- } catch (error) {
362
- const errorDiv = document.createElement('div');
363
- errorDiv.className = 'ai-feedback';
364
- errorDiv.innerHTML = `<p class="text-red-600 font-semibold">Failed to get feedback: ${error.message}</p>`;
365
- activityChatDisplay.appendChild(errorDiv);
366
- } finally {
367
- submitActivityBtn.disabled = false;
368
- submitActivityBtn.textContent = 'Submit Answer';
369
- }
370
- }
371
-
372
- // CONVERSAÇÃO
373
- function initializeSpeechRecognition() {
374
- window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
375
- if (!window.SpeechRecognition) {
376
- micButton.disabled = true;
377
- micButton.textContent = '❌';
378
- alert('Voice recognition API is not supported in this browser.');
379
- return;
380
- }
381
- recognition = new SpeechRecognition();
382
- recognition.lang = 'en-US';
383
- recognition.interimResults = false;
384
- recognition.continuous = false;
385
- recognition.onresult = (event) => {
386
- const transcript = event.results[0][0].transcript;
387
- chatInput.value = transcript;
388
- sendMessage(transcript);
389
- };
390
- recognition.onerror = (event) => { console.error('Voice recognition error:', event.error); };
391
- recognition.onend = () => {
392
- isRecording = false;
393
- micButton.classList.remove('is-recording');
394
- micButton.innerHTML = '🎙️';
395
- };
396
- }
397
- function handleVoiceInput() {
398
- if (isRecording) {
399
- recognition.stop();
400
- } else {
401
- isRecording = true;
402
- micButton.classList.add('is-recording');
403
- micButton.innerHTML = '<div class="spinner"></div>';
404
- recognition.start();
405
- }
406
- }
407
- function handleTextInput() {
408
- const message = chatInput.value.trim();
409
- sendMessage(message);
410
- chatInput.value = '';
411
- }
412
- chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleTextInput(); });
413
- async function sendMessage(message) {
414
- if (!message) return;
415
- appendMessage(message, 'user');
416
- chatHistory.push({ role: 'user', content: message });
417
- try {
418
- const response = await fetch('/chat-with-ai', {
419
- method: 'POST',
420
- headers: { 'Content-Type': 'application/json' },
421
- body: JSON.stringify({ history: chatHistory, message: message })
422
- });
423
- if (!response.ok) throw new Error((await response.json()).error);
424
- const data = await response.json();
425
- appendMessage(data.response, 'ai');
426
- chatHistory.push({ role: 'assistant', content: data.response });
427
- playAudio(data.response);
428
- } catch (error) {
429
- appendMessage(`Chat error: ${error.message}`, 'ai');
430
- }
431
- }
432
- function appendMessage(text, sender) {
433
- const msg = document.createElement('div');
434
- msg.className = `chat-message ${sender}-message`;
435
- msg.textContent = text;
436
- chatDisplay.appendChild(msg);
437
- chatDisplay.scrollTop = chatDisplay.scrollHeight;
438
- }
439
-
440
- // IMAGEM
441
- imageUploader.addEventListener('change', (event) => {
442
- const file = event.target.files[0];
443
- if (!file) return;
444
- const reader = new FileReader();
445
- reader.onload = (e) => {
446
- imagePreview.src = e.target.result;
447
- imagePreview.classList.remove('hidden');
448
- analyzeImage(e.target.result);
449
- };
450
- reader.readAsDataURL(file);
451
- });
452
- async function analyzeImage(base64Image) {
453
- 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>`;
454
- try {
455
- const response = await fetch('/analyze-image', {
456
- method: 'POST',
457
- headers: { 'Content-Type': 'application/json' },
458
- body: JSON.stringify({
459
- image: base64Image,
460
- model: modelSelector.value
461
- })
462
- });
463
- if (!response.ok) throw new Error((await response.json()).error);
464
- const vocabulary = await response.json();
465
- displayImageVocabulary(vocabulary);
466
- } catch (error) {
467
- imageAnalysisResult.innerHTML = `<p class="text-red-600 font-semibold">Analysis failed: ${error.message}</p>`;
468
- }
469
- }
470
- function displayImageVocabulary(vocabulary) {
471
- imageAnalysisResult.innerHTML = '<h3 class="font-semibold mb-2">Suggested Vocabulary:</h3>';
472
- const buttonContainer = document.createElement('div');
473
- buttonContainer.className = 'flex flex-wrap gap-2';
474
- vocabulary.forEach(({ term, definition }) => {
475
- const btn = document.createElement('button');
476
- btn.className = 'btn bg-violet-100 text-violet-700 hover:bg-violet-200 text-sm';
477
- btn.textContent = term;
478
- btn.onclick = () => createFlashcardFromSuggestion(term, definition);
479
- buttonContainer.appendChild(btn);
480
- });
481
- imageAnalysisResult.appendChild(buttonContainer);
482
- }
483
- async function generateImage() {
484
- const prompt = imagePromptInput.value.trim();
485
- if (!prompt) {
486
- alert('Please enter a description for the image.');
487
- return;
488
- }
489
- generateImageBtn.disabled = true;
490
- generateImageBtn.innerHTML = '<div class="spinner mr-2"></div> Generating...';
491
- 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>`;
492
- try {
493
- const response = await fetch('/generate-image', {
494
- method: 'POST',
495
- headers: { 'Content-Type': 'application/json' },
496
- body: JSON.stringify({ prompt: prompt })
497
- });
498
- if (!response.ok) throw new Error((await response.json()).error);
499
- const data = await response.json();
500
- imageResultContainer.innerHTML = `<img src="data:image/png;base64,${data.image_base64}" class="rounded-lg max-h-60 mx-auto" alt="AI-generated image"/>`;
501
- } catch (error) {
502
- imageResultContainer.innerHTML = `<p class="text-red-600 font-semibold">Failed to generate image: ${error.message}</p>`;
503
- } finally {
504
- generateImageBtn.disabled = false;
505
- generateImageBtn.textContent = 'Generate';
506
- }
507
- }
508
-
509
- // FLASHCARDS
510
- async function createFlashcard(word, context, definition = null) {
511
- const btn = definition ? null : createCardButton;
512
- if (btn) {
513
- btn.disabled = true;
514
- btn.innerHTML = '<div class="spinner mr-2"></div> Creating...';
515
- }
516
- try {
517
- const response = await fetch('/explain-proxy', {
518
- method: 'POST',
519
- headers: { 'Content-Type': 'application/json' },
520
- body: JSON.stringify({
521
- word: word,
522
- context: context,
523
- for_flashcard: true,
524
- model: modelSelector.value,
525
- context_focus: contextSelector.value
526
- })
527
- });
528
- if (!response.ok) {
529
- const errData = await response.json();
530
- throw new Error(errData.error || 'Unknown server error.');
531
- }
532
- const cardData = await response.json();
533
- if(definition && !cardData.definition) cardData.definition = definition;
534
- renderFlashcard(cardData);
535
- } catch (error) {
536
- alert(`Failed to create flashcard: ${error.message}. Check API keys and model selection.`);
537
- } finally {
538
- if (btn) {
539
- btn.disabled = false;
540
- btn.innerHTML = '+ Create Flashcard';
541
- }
542
- }
543
- }
544
- function createFlashcardFromSelection() {
545
- const selection = window.getSelection().toString().trim();
546
- if (selection) createFlashcard(selection, textEditor.innerText);
547
- }
548
- function createFlashcardFromSuggestion(term, definition) {
549
- createFlashcard(term, `(From image analysis) A visual representation of ${term}.`, definition);
550
- }
551
- function renderFlashcard(data) {
552
- noCardsMessage.classList.add('hidden');
553
- const cardId = `card-${Date.now()}`;
554
- const container = document.createElement('div');
555
- container.className = 'flashcard-container';
556
- container.id = cardId;
557
- const minHeight = 380;
558
- container.style.minHeight = `${minHeight}px`;
559
- const fallback = 'Info not generated';
560
- container.innerHTML = `
561
- <button class="delete-btn" onclick="deleteFlashcard('${cardId}', event)">🗑️</button>
562
- <div class="flashcard-inner" style="min-height: ${minHeight}px;">
563
- <div class="flashcard-front">
564
- <div>
565
- <div class="text-sm text-gray-600 mb-2">Context Sentence:</div>
566
- <p class="text-lg text-center text-gray-800">${data.gapped_sentence || fallback}</p>
567
- </div>
568
- <div class="border-t pt-4 mt-4 text-center">
569
- <strong class="text-indigo-600">Hint (PT):</strong> ${data.translation || fallback}
570
- </div>
571
- <div class="text-xs text-center text-gray-400 mt-4">Click to see the answer</div>
572
- </div>
573
- <div class="flashcard-back">
574
- <div>
575
- <h3 class="text-xl font-bold text-center text-indigo-600 mb-2">${data.term || 'Term'}</h3>
576
- <p class="flex items-center justify-center gap-2 text-gray-600 italic mb-3">
577
- <button class="audio-btn" onclick="playAudio('${(data.context_sentence || '').replace(/'/g, "\\'")}', event)">🔊</button>
578
- <span>${data.context_sentence || fallback}</span>
579
- </p>
580
- <p class="text-sm text-gray-800"><strong class="font-semibold">Definition:</strong> ${data.definition || fallback}</p>
581
- </div>
582
- <div class="border-t mt-3 pt-3">
583
- <h4 class="text-sm font-semibold text-center">Practice Your Pronunciation</h4>
584
- <div class="flex justify-center items-center gap-2 mt-2">
585
- <button class="btn btn-primary w-12 h-12 rounded-full" onclick="handlePronunciationPractice(this, '${(data.context_sentence || '').replace(/'/g, "\\'")}', event)">🎙️</button>
586
- </div>
587
- <div class="text-xs text-gray-600 p-2 mt-2 bg-gray-50 rounded-md min-h-[40px]" data-feedback-area></div>
588
- </div>
589
- </div>
590
- </div>`;
591
- flashcardList.prepend(container);
592
- container.addEventListener('click', (e) => {
593
- if(e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
594
- container.classList.toggle('flipped');
595
- }
596
- });
597
- }
598
- function deleteFlashcard(cardId, event) {
599
- event.stopPropagation();
600
- const cardToDelete = document.getElementById(cardId);
601
- if (cardToDelete) {
602
- cardToDelete.remove();
603
- }
604
- if (flashcardList.children.length === 1) {
605
- noCardsMessage.classList.remove('hidden');
606
- }
607
- }
608
- async function handlePronunciationPractice(button, targetText, event) {
609
- event.stopPropagation();
610
- const feedbackArea = button.closest('.flashcard-back').querySelector('[data-feedback-area]');
611
- const practiceRecognition = new SpeechRecognition();
612
- practiceRecognition.lang = 'en-US';
613
- button.innerHTML = '<div class="spinner"></div>';
614
- button.disabled = true;
615
- feedbackArea.textContent = 'Listening...';
616
- practiceRecognition.start();
617
- practiceRecognition.onresult = async (e) => {
618
- const userText = e.results[0][0].transcript;
619
- feedbackArea.textContent = `You said: "${userText}". Analyzing...`;
620
- try {
621
- const response = await fetch('/pronunciation-feedback', {
622
- method: 'POST',
623
- headers: { 'Content-Type': 'application/json' },
624
- body: JSON.stringify({ target_text: targetText, user_text: userText })
625
- });
626
- if (!response.ok) throw new Error((await response.json()).error);
627
- const data = await response.json();
628
- feedbackArea.innerHTML = `<strong>Feedback:</strong> ${data.feedback}`;
629
- } catch (error) {
630
- feedbackArea.textContent = `Analysis error: ${error.message}`;
631
- } finally {
632
- button.innerHTML = '🎙️';
633
- button.disabled = false;
634
- }
635
- };
636
- practiceRecognition.onerror = (e) => {
637
- feedbackArea.textContent = `Recording error: ${e.error}`;
638
- button.innerHTML = '🎙️';
639
- button.disabled = false;
640
- };
641
- }
642
-
643
- // --- INICIALIZAÇÃO ---
644
- document.addEventListener('DOMContentLoaded', () => {
645
- populateModelSelector();
646
- initializeSpeechRecognition();
647
- });
648
- </script>
649
-
650
- </body>
651
- </html>
652
-