amauricunha commited on
Commit
74229f5
·
verified ·
1 Parent(s): 7b254d2

Delete index.html

Browse files
Files changed (1) hide show
  1. index.html +0 -1959
index.html DELETED
@@ -1,1959 +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
-
34
- /* Toast Notifications */
35
- .toast-container { position: fixed; top: 20px; right: 20px; z-index: 1000; max-width: 400px; }
36
- .toast { background: white; border-left: 4px solid #4f46e5; border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.15); padding: 1rem; margin-bottom: 0.5rem; opacity: 0; transform: translateX(100%); transition: all 0.3s ease; }
37
- .toast.show { opacity: 1; transform: translateX(0); }
38
- .toast.error { border-left-color: #ef4444; }
39
- .toast.success { border-left-color: #10b981; }
40
- .toast.warning { border-left-color: #f59e0b; }
41
- .toast-close { float: right; background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #9ca3af; }
42
-
43
- /* Loading States & Skeleton */
44
- .loading-skeleton {
45
- background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
46
- background-size: 200% 100%;
47
- animation: loading 1.5s infinite;
48
- border-radius: 4px;
49
- }
50
- @keyframes loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
51
-
52
- .skeleton-text { height: 1rem; margin: 0.25rem 0; }
53
- .skeleton-text.large { height: 1.5rem; }
54
- .skeleton-text.small { height: 0.75rem; }
55
- .skeleton-box { height: 100px; border-radius: 8px; }
56
-
57
- /* Enhanced Loading Spinner */
58
- .spinner-container { display: flex; align-items: center; justify-content: center; padding: 2rem; }
59
- .spinner-large { width: 40px; height: 40px; border-width: 4px; }
60
-
61
- /* Fade in animations */
62
- .fade-in { animation: fadeIn 0.5s ease-in; }
63
- @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
64
-
65
- /* Slide in animations */
66
- .slide-in { animation: slideIn 0.3s ease-out; }
67
- @keyframes slideIn { from { transform: translateX(-20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
68
-
69
- /* Empty states */
70
- .empty-state {
71
- text-align: center; padding: 3rem 1rem; color: #6b7280;
72
- background: #f9fafb; border: 2px dashed #d1d5db; border-radius: 12px; margin: 1rem 0;
73
- }
74
- .empty-state-icon { font-size: 3rem; margin-bottom: 1rem; opacity: 0.5; }
75
- .empty-state-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; color: #374151; }
76
- .empty-state-description { font-size: 0.875rem; line-height: 1.5; }
77
-
78
- /* Enhanced Flashcards - Responsive */
79
- .flashcard-container { perspective: 1000px; position: relative; }
80
- .flashcard-inner { position: relative; width: 100%; min-height: 280px; transition: transform 0.6s; transform-style: preserve-3d; }
81
- .flashcard-container.flipped .flashcard-inner { transform: rotateY(180deg); }
82
- .flashcard-front, .flashcard-back {
83
- position: absolute; width: 100%; height: 100%; min-height: 280px; backface-visibility: hidden;
84
- border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; display: flex; flex-direction: column;
85
- justify-content: space-between; word-wrap: break-word; overflow-wrap: break-word;
86
- }
87
- .flashcard-front { background-color: #f9fafb; }
88
- .flashcard-back { background-color: #ffffff; transform: rotateY(180deg); }
89
-
90
- /* Responsive text handling */
91
- .flashcard-text {
92
- font-size: 1rem; line-height: 1.5; max-height: 150px; overflow-y: auto;
93
- word-break: break-word; hyphens: auto; padding: 0.25rem 0;
94
- }
95
-
96
- /* Mobile responsiveness */
97
- @media (max-width: 768px) {
98
- .flashcard-text { font-size: 0.9rem; max-height: 120px; line-height: 1.4; }
99
- .flashcard-container { margin-bottom: 1rem; }
100
- .flashcard-inner { min-height: 250px; }
101
- .flashcard-front, .flashcard-back { min-height: 250px; padding: 16px; }
102
- .card { padding: 16px; }
103
- body { padding: 0.5rem; }
104
- .container { padding: 0; }
105
-
106
- /* Adjust grid for mobile */
107
- .grid.grid-cols-1.md\\:grid-cols-3 { grid-template-columns: 1fr; }
108
- .grid.grid-cols-1.lg\\:grid-cols-2 { grid-template-columns: 1fr; }
109
- }
110
-
111
- @media (max-width: 480px) {
112
- .flashcard-text { font-size: 0.85rem; max-height: 100px; }
113
- .flashcard-inner { min-height: 220px; }
114
- .flashcard-front, .flashcard-back { min-height: 220px; padding: 12px; }
115
-
116
- /* Make buttons more touch-friendly */
117
- .btn { padding: 0.75rem 1rem; min-height: 44px; }
118
- .audio-btn { font-size: 1.25rem; padding: 0.5rem; }
119
- .delete-btn { font-size: 1rem; padding: 0.5rem; }
120
- }
121
-
122
- /* Text overflow handling */
123
- .text-truncate {
124
- overflow: hidden;
125
- text-overflow: ellipsis;
126
- white-space: nowrap;
127
- }
128
-
129
- .text-wrap {
130
- white-space: normal;
131
- word-wrap: break-word;
132
- overflow-wrap: break-word;
133
- }
134
-
135
- /* Form Validation */
136
- .form-error { border-color: #ef4444 !important; }
137
- .error-message { color: #ef4444; font-size: 0.875rem; margin-top: 0.25rem; }
138
-
139
- /* Enhanced buttons */
140
- .btn { position: relative; overflow: hidden; }
141
- .btn::after { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; border-radius: 50%; background: rgba(255,255,255,0.3); transform: translate(-50%, -50%); transition: width 0.3s, height 0.3s; }
142
- .btn:active::after { width: 300px; height: 300px; }
143
-
144
- /* Pulse animation for mic button */
145
- .mic-button.is-recording { animation: pulse 1.5s infinite; }
146
- @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } }
147
-
148
- /* Auth Modal */
149
- .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
150
- .modal.show { display: block; }
151
- .modal-content {
152
- background-color: white; margin: 5% auto; padding: 0; width: 90%; max-width: 400px;
153
- border-radius: 12px; position: relative; animation: modalSlideIn 0.3s ease;
154
- }
155
- @keyframes modalSlideIn { from { opacity: 0; transform: translateY(-50px); } to { opacity: 1; transform: translateY(0); } }
156
- .modal-header { padding: 1.5rem; border-bottom: 1px solid #e5e7eb; }
157
- .modal-body { padding: 1.5rem; }
158
- .modal-footer { padding: 1rem 1.5rem; border-top: 1px solid #e5e7eb; display: flex; justify-content: flex-end; gap: 0.5rem; }
159
- .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
160
- .close:hover { color: #000; }
161
-
162
- /* Auth Form */
163
- .auth-form { display: none; }
164
- .auth-form.active { display: block; }
165
- .form-group { margin-bottom: 1rem; }
166
- .form-group label { display: block; margin-bottom: 0.25rem; font-weight: 500; color: #374151; }
167
- .form-group input { width: 100%; padding: 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.875rem; }
168
- .form-group input:focus { outline: none; border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); }
169
-
170
- /* User Menu */
171
- .user-menu { position: relative; display: inline-block; }
172
- .user-menu-content { display: none; position: absolute; right: 0; background-color: white; min-width: 200px; box-shadow: 0 8px 16px rgba(0,0,0,0.1); border-radius: 8px; z-index: 1; border: 1px solid #e5e7eb; }
173
- .user-menu.show .user-menu-content { display: block; }
174
- .user-menu-item { padding: 0.75rem 1rem; display: block; color: #374151; text-decoration: none; border-bottom: 1px solid #f3f4f6; }
175
- .user-menu-item:hover { background-color: #f9fafb; }
176
- .user-menu-item:last-child { border-bottom: none; }
177
-
178
- /* Header Auth Section */
179
- .auth-section { display: flex; align-items: center; gap: 0.5rem; }
180
- .user-avatar { width: 40px; height: 40px; border-radius: 50%; background-color: #4f46e5; color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; cursor: pointer; }
181
-
182
- /* Content Curation Styles */
183
- .content-item {
184
- border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; margin-bottom: 1rem;
185
- background: white; transition: all 0.2s;
186
- }
187
- .content-item:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); border-color: #4f46e5; }
188
- .content-item-title { font-weight: 600; color: #1f2937; margin-bottom: 0.5rem; }
189
- .content-item-meta { font-size: 0.75rem; color: #6b7280; margin-bottom: 0.5rem; }
190
- .content-item-summary { font-size: 0.875rem; color: #4b5563; line-height: 1.5; }
191
- .difficulty-badge {
192
- display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 500;
193
- }
194
- .difficulty-easy { background-color: #dcfce7; color: #166534; }
195
- .difficulty-medium { background-color: #fef3c7; color: #92400e; }
196
- .difficulty-hard { background-color: #fee2e2; color: #991b1b; }
197
-
198
- /* Analytics Styles */
199
- .metric-card {
200
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
201
- color: white; border-radius: 12px; padding: 1.5rem; text-align: center;
202
- }
203
- .metric-value { font-size: 2rem; font-weight: bold; margin-bottom: 0.5rem; }
204
- .metric-label { font-size: 0.875rem; opacity: 0.9; }
205
-
206
- /* Study Plan Styles */
207
- .plan-activity {
208
- display: flex; align-items: center; padding: 0.75rem; border-radius: 6px;
209
- border-left: 4px solid #4f46e5; background-color: #f8fafc; margin-bottom: 0.5rem;
210
- }
211
- .plan-activity-icon { margin-right: 0.75rem; font-size: 1.25rem; }
212
- .plan-activity-text { flex-grow: 1; font-size: 0.875rem; }
213
- .plan-activity-time { font-size: 0.75rem; color: #6b7280; }
214
- </style>
215
- </head>
216
- <body class="p-4 md:p-8">
217
- <!-- Toast Container -->
218
- <div id="toastContainer" class="toast-container"></div>
219
-
220
- <div class="container mx-auto max-w-5xl">
221
- <header class="mb-10">
222
- <div class="flex justify-between items-center mb-6">
223
- <div>
224
- <h1 class="text-3xl lg:text-4xl font-bold text-gray-800">Dynamic English Study Studio</h1>
225
- <p class="text-gray-500 mt-2">Your study tool with audio, images, and conversation & pronunciation practice.</p>
226
- </div>
227
- <div class="auth-section">
228
- <!-- Guest state -->
229
- <div id="guestAuth" class="flex gap-2">
230
- <button onclick="showAuthModal('login')" class="btn btn-primary">Login</button>
231
- <button onclick="showAuthModal('register')" class="btn bg-gray-500 hover:bg-gray-600 text-white">Sign Up</button>
232
- </div>
233
-
234
- <!-- Authenticated state -->
235
- <div id="userAuth" class="user-menu" style="display: none;">
236
- <div class="user-avatar" onclick="toggleUserMenu()" id="userAvatar">U</div>
237
- <div class="user-menu-content">
238
- <div class="user-menu-item" style="font-weight: 600; color: #4f46e5;" id="userEmail">user@example.com</div>
239
- <a href="#" onclick="showUserSettings()" class="user-menu-item">⚙️ Settings</a>
240
- <a href="#" onclick="showUserFlashcards()" class="user-menu-item">📚 My Flashcards</a>
241
- <a href="#" onclick="logout()" class="user-menu-item">🚪 Logout</a>
242
- </div>
243
- </div>
244
- </div>
245
- </div>
246
- </header>
247
-
248
- <div class="card mb-8">
249
- <h2 class="text-xl font-semibold mb-4 text-gray-700">Global Session Settings</h2>
250
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
251
- <div>
252
- <label for="modelSelector" class="block text-sm font-medium text-gray-700">Main AI Model</label>
253
- <select id="modelSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
254
- <option>Loading models...</option>
255
- </select>
256
- </div>
257
- <div>
258
- <label for="contextSelector" class="block text-sm font-medium text-gray-700">Vocabulary Focus</label>
259
- <select id="contextSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
260
- <option value="General/Social">General/Social</option>
261
- <option value="Professional/Business">Professional/Business</option>
262
- <option value="Technical/IT">Technical/IT</option>
263
- </select>
264
- </div>
265
- <div>
266
- <label for="voiceSelector" class="block text-sm font-medium text-gray-700">Voice Accent (TTS)</label>
267
- <select id="voiceSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
268
- <option value="co.uk">British (Female)</option>
269
- <option value="com">American (Female)</option>
270
- <option value="com.au">Australian (Female)</option>
271
- </select>
272
- </div>
273
- </div>
274
- </div>
275
-
276
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
277
- <!-- Coluna da Esquerda: Inputs -->
278
- <div class="flex flex-col gap-6">
279
- <div class="card">
280
- <h2 class="text-xl font-semibold mb-4 text-gray-700">1. Study from Text</h2>
281
- <div id="textEditor" contenteditable="true" spellcheck="true" lang="en" class="w-full p-4 border rounded-md min-h-[150px]"></div>
282
- <p class="text-sm text-right text-gray-500 mt-2">Characters: <span id="charCount">0</span>/10000</p>
283
- <div class="mt-4 border-t pt-4 flex flex-wrap gap-2 justify-between items-center">
284
- <button id="createCardButton" onclick="createFlashcardFromSelection()" class="btn btn-primary bg-green-600 hover:bg-green-700 disabled:opacity-50" disabled>+ Create Flashcard</button>
285
- <button id="playButton" onclick="handlePlay()" class="btn btn-primary" disabled>
286
- <span id="playText">Listen to Text</span>
287
- <div id="playSpinner" class="spinner ml-2 hidden"></div>
288
- </button>
289
- </div>
290
- <audio id="audioPlayer" controls class="w-full mt-4 hidden"></audio>
291
- </div>
292
-
293
- <div class="card">
294
- <h2 class="text-xl font-semibold mb-4 text-gray-700">2. Study from Image (Analysis)</h2>
295
- <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"/>
296
- <img id="imagePreview" src="" class="mt-4 rounded-lg hidden max-h-60 mx-auto" alt="Preview"/>
297
- <div id="imageAnalysisResult" class="mt-4"></div>
298
- </div>
299
-
300
- <div class="card">
301
- <h2 class="text-xl font-semibold mb-4 text-gray-700">3. Conversation Practice</h2>
302
- <div id="chatDisplay" class="flex flex-col space-y-2 mb-4"></div>
303
- <div class="flex gap-2">
304
- <input type="text" id="chatInput" class="flex-grow border rounded-lg px-3 py-2" placeholder="Type or use the microphone...">
305
- <button onclick="handleTextInput()" class="btn btn-primary bg-gray-500 hover:bg-gray-600">Send</button>
306
- <button id="micButton" onclick="handleVoiceInput()" class="btn btn-primary mic-button w-12 h-12 rounded-full">🎙️</button>
307
- </div>
308
- </div>
309
-
310
- <div class="card">
311
- <h2 class="text-xl font-semibold mb-4 text-gray-700">4. Image Generator (Nano Banana)</h2>
312
- <p class="text-sm text-gray-600 mb-2">Describe a scene in English for the AI to draw.</p>
313
- <div class="flex gap-2">
314
- <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">
315
- <button id="generateImageBtn" onclick="generateImage()" class="btn btn-primary">Generate</button>
316
- </div>
317
- <div id="imageResultContainer" class="mt-4 p-4 border rounded-lg bg-gray-50 min-h-[200px] flex items-center justify-center">
318
- <p class="text-gray-500 italic">Your image will appear here.</p>
319
- </div>
320
- </div>
321
-
322
- <div class="card">
323
- <h2 class="text-xl font-semibold mb-4 text-gray-700">5. Content Discovery & Curation</h2>
324
- <div id="contentCuration" style="display: none;">
325
- <div class="mb-4">
326
- <h3 class="text-lg font-medium mb-2">🔍 Find Relevant Content</h3>
327
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
328
- <div>
329
- <label class="block text-sm font-medium text-gray-700 mb-1">Search Topic</label>
330
- <input type="text" id="contentSearchInput" class="w-full border rounded-lg px-3 py-2" placeholder="e.g., cybersecurity, product management">
331
- </div>
332
- <div>
333
- <label class="block text-sm font-medium text-gray-700 mb-1">Category</label>
334
- <select id="contentCategorySelect" class="w-full border rounded-lg px-3 py-2">
335
- <option value="">All Categories</option>
336
- <option value="technology">Technology</option>
337
- <option value="business">Business</option>
338
- <option value="automotive">Automotive</option>
339
- <option value="cybersecurity">Cybersecurity</option>
340
- <option value="science">Science</option>
341
- </select>
342
- </div>
343
- </div>
344
- <div class="flex gap-2 mb-4">
345
- <button onclick="searchContent()" class="btn btn-primary">🔍 Search Articles</button>
346
- <button onclick="getRecommendations()" class="btn bg-purple-500 hover:bg-purple-600 text-white">✨ AI Recommendations</button>
347
- <button onclick="showSavedArticles()" class="btn bg-green-500 hover:bg-green-600 text-white">📚 Saved Articles</button>
348
- </div>
349
- </div>
350
-
351
- <div id="contentResults" class="space-y-4">
352
- <div class="empty-state">
353
- <div class="empty-state-icon">🔍</div>
354
- <div class="empty-state-title">Discover Personalized Content</div>
355
- <div class="empty-state-description">Search for articles or get AI recommendations based on your interests and English level</div>
356
- </div>
357
- </div>
358
-
359
- <div class="mt-6 pt-4 border-t">
360
- <h3 class="text-lg font-medium mb-2">📝 Add Your Own Content</h3>
361
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
362
- <input type="text" id="customContentTitle" class="border rounded-lg px-3 py-2" placeholder="Article Title">
363
- <input type="url" id="customContentUrl" class="border rounded-lg px-3 py-2" placeholder="URL (optional)">
364
- </div>
365
- <textarea id="customContentText" rows="4" class="w-full border rounded-lg px-3 py-2 mb-4" placeholder="Paste your text content here..."></textarea>
366
- <button onclick="saveCustomContent()" class="btn btn-primary">💾 Save Content</button>
367
- </div>
368
- </div>
369
- <div id="contentCurationLogin" class="text-center py-8">
370
- <div class="empty-state">
371
- <div class="empty-state-icon">🔐</div>
372
- <div class="empty-state-title">Login Required</div>
373
- <div class="empty-state-description">
374
- Please log in to access personalized content curation, save articles, and get AI-powered recommendations.
375
- </div>
376
- <button onclick="showAuthModal('login')" class="btn btn-primary mt-4">Login to Continue</button>
377
- </div>
378
- </div>
379
- </div>
380
-
381
- <div class="card">
382
- <h2 class="text-xl font-semibold mb-4 text-gray-700">6. Interactive Writing Practice</h2>
383
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
384
- <div>
385
- <label for="activityTypeSelector" class="block text-sm font-medium text-gray-700">Activity Type</label>
386
- <select id="activityTypeSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
387
- <option value="translate_pt_en">Translate (PT to EN)</option>
388
- <option value="email_writing">Write Formal Email</option>
389
- <option value="summary_writing">Summarize Main Text</option>
390
- <option value="professional_conversation">Simulate Professional Conversation</option>
391
- </select>
392
- </div>
393
- <div class="flex items-end">
394
- <button id="generateActivityButton" onclick="generateActivity()" class="btn btn-primary w-full">Generate Activity</button>
395
- </div>
396
- </div>
397
-
398
- <div id="activityChatContainer" class="mt-4 border-t pt-4">
399
- <div id="activityChatDisplay">
400
- <p class="text-gray-500 italic">Your interactive exercise will appear here.</p>
401
- </div>
402
- <div id="activityResponseArea" class="mt-4 hidden">
403
- <label for="activityInput" class="block text-sm font-medium text-gray-700 mb-1">Your Answer:</label>
404
- <textarea id="activityInput" rows="4" class="w-full p-2 border rounded-md"></textarea>
405
- <button id="submitActivityBtn" onclick="submitActivityResponse()" class="btn btn-primary mt-2">Submit Answer</button>
406
- </div>
407
- </div>
408
- </div>
409
- </div>
410
-
411
- <!-- Coluna da Direita: Painel Dinâmico -->
412
- <div class="flex flex-col gap-6">
413
- <!-- Study Analytics Panel (for logged users) -->
414
- <div id="analyticsPanel" class="card" style="display: none;">
415
- <div class="flex justify-between items-center mb-4">
416
- <h2 class="text-xl font-semibold text-gray-700">📊 Your Progress</h2>
417
- <button onclick="toggleAnalytics()" class="text-gray-500 hover:text-gray-700">↕️</button>
418
- </div>
419
- <div id="analyticsContent">
420
- <div class="grid grid-cols-2 gap-4 mb-4">
421
- <div class="text-center p-4 bg-blue-50 rounded-lg">
422
- <div class="text-2xl font-bold text-blue-600" id="totalFlashcards">0</div>
423
- <div class="text-sm text-gray-600">Flashcards</div>
424
- </div>
425
- <div class="text-center p-4 bg-green-50 rounded-lg">
426
- <div class="text-2xl font-bold text-green-600" id="totalArticles">0</div>
427
- <div class="text-sm text-gray-600">Articles</div>
428
- </div>
429
- </div>
430
- <div class="text-center p-3 bg-purple-50 rounded-lg">
431
- <div class="text-lg font-semibold text-purple-600" id="currentLevel">B1</div>
432
- <div class="text-sm text-gray-600">Current Level</div>
433
- </div>
434
- </div>
435
- </div>
436
-
437
- <!-- Study Planner Panel (for logged users) -->
438
- <div id="studyPlannerPanel" class="card" style="display: none;">
439
- <h2 class="text-xl font-semibold mb-4 text-gray-700">🎯 Study Planner</h2>
440
- <div id="studyPlanContent">
441
- <div class="mb-4">
442
- <h3 class="font-medium mb-2">Quick Plan Generator</h3>
443
- <div class="grid grid-cols-1 gap-2 mb-3">
444
- <select id="targetLevelSelect" class="border rounded-lg px-3 py-2 text-sm">
445
- <option value="A2">Target: A2 (Elementary)</option>
446
- <option value="B1">Target: B1 (Intermediate)</option>
447
- <option value="B2" selected>Target: B2 (Upper-Intermediate)</option>
448
- <option value="C1">Target: C1 (Advanced)</option>
449
- <option value="C2">Target: C2 (Proficient)</option>
450
- </select>
451
- <select id="weeklyHoursSelect" class="border rounded-lg px-3 py-2 text-sm">
452
- <option value="3">3 hours/week</option>
453
- <option value="5" selected>5 hours/week</option>
454
- <option value="7">7 hours/week</option>
455
- <option value="10">10 hours/week</option>
456
- </select>
457
- </div>
458
- <button onclick="generateStudyPlan()" class="btn btn-primary w-full text-sm">🚀 Generate Plan</button>
459
- </div>
460
- <div id="studyPlanResult" class="text-sm text-gray-600">
461
- Your personalized study plan will appear here
462
- </div>
463
- </div>
464
- </div>
465
-
466
- <!-- Flashcards Panel -->
467
- <div id="flashcardsPanel" class="card">
468
- <div class="flex justify-between items-center mb-4">
469
- <h2 class="text-xl font-semibold text-gray-700">Your Study Session</h2>
470
- <div class="flex items-center gap-2">
471
- <button id="loadFlashcardsBtn" onclick="showUserFlashcards()" class="btn bg-blue-100 text-blue-700 hover:bg-blue-200 text-sm" style="display: none;" title="Load your saved flashcards">📚 Load Saved</button>
472
- <div id="sessionStatus" class="text-sm">
473
- <span id="guestStatus" class="text-gray-500">Guest Mode - Progress not saved</span>
474
- <span id="userStatus" class="text-green-600" style="display: none;">✓ Progress being saved</span>
475
- </div>
476
- </div>
477
- </div>
478
- <div id="flashcardList" class="space-y-6">
479
- <div id="noCardsMessage" class="empty-state">
480
- <div class="empty-state-icon">📚</div>
481
- <div class="empty-state-title">No flashcards yet</div>
482
- <div class="empty-state-description">
483
- Select text from the editor or analyze an image to create your first flashcard!
484
- </div>
485
- </div>
486
- </div>
487
- </div>
488
- </div>
489
- </div>
490
- </div>
491
-
492
- <!-- Auth Modal -->
493
- <div id="authModal" class="modal">
494
- <div class="modal-content">
495
- <div class="modal-header">
496
- <h2 id="authModalTitle">Login</h2>
497
- <span class="close" onclick="hideAuthModal()">&times;</span>
498
- </div>
499
- <div class="modal-body">
500
- <!-- Login Form -->
501
- <form id="loginForm" class="auth-form active">
502
- <div class="form-group">
503
- <label for="loginEmail">Email</label>
504
- <input type="email" id="loginEmail" required>
505
- </div>
506
- <div class="form-group">
507
- <label for="loginPassword">Password</label>
508
- <input type="password" id="loginPassword" required>
509
- </div>
510
- <button type="submit" class="btn btn-primary w-full">Login</button>
511
- <p class="text-center mt-4 text-sm text-gray-600">
512
- Don't have an account?
513
- <a href="#" onclick="switchAuthForm('register')" class="text-indigo-600 hover:text-indigo-800">Sign up</a>
514
- </p>
515
- </form>
516
-
517
- <!-- Register Form -->
518
- <form id="registerForm" class="auth-form">
519
- <div class="form-group">
520
- <label for="registerEmail">Email</label>
521
- <input type="email" id="registerEmail" required>
522
- </div>
523
- <div class="form-group">
524
- <label for="registerPassword">Password</label>
525
- <input type="password" id="registerPassword" required minlength="8">
526
- <small class="text-gray-500">At least 8 characters</small>
527
- </div>
528
- <div class="form-group">
529
- <label for="confirmPassword">Confirm Password</label>
530
- <input type="password" id="confirmPassword" required>
531
- </div>
532
- <button type="submit" class="btn btn-primary w-full">Create Account</button>
533
- <p class="text-center mt-4 text-sm text-gray-600">
534
- Already have an account?
535
- <a href="#" onclick="switchAuthForm('login')" class="text-indigo-600 hover:text-indigo-800">Login</a>
536
- </p>
537
- </form>
538
- </div>
539
- </div>
540
- </div>
541
-
542
- <script>
543
- // --- ELEMENTOS GLOBAIS ---
544
- const modelSelector = document.getElementById('modelSelector');
545
- const contextSelector = document.getElementById('contextSelector');
546
- const voiceSelector = document.getElementById('voiceSelector');
547
- const textEditor = document.getElementById('textEditor');
548
- const createCardButton = document.getElementById('createCardButton');
549
- const playButton = document.getElementById('playButton');
550
- const audioPlayer = document.getElementById('audioPlayer');
551
- const flashcardList = document.getElementById('flashcardList');
552
- const noCardsMessage = document.getElementById('noCardsMessage');
553
- const imageUploader = document.getElementById('imageUploader');
554
- const imagePreview = document.getElementById('imagePreview');
555
- const imageAnalysisResult = document.getElementById('imageAnalysisResult');
556
- const chatDisplay = document.getElementById('chatDisplay');
557
- const chatInput = document.getElementById('chatInput');
558
- const micButton = document.getElementById('micButton');
559
- const imagePromptInput = document.getElementById('imagePromptInput');
560
- const generateImageBtn = document.getElementById('generateImageBtn');
561
- const imageResultContainer = document.getElementById('imageResultContainer');
562
- const activityTypeSelector = document.getElementById('activityTypeSelector');
563
- const generateActivityButton = document.getElementById('generateActivityButton');
564
- const activityChatDisplay = document.getElementById('activityChatDisplay');
565
- const activityResponseArea = document.getElementById('activityResponseArea');
566
- const activityInput = document.getElementById('activityInput');
567
- const submitActivityBtn = document.getElementById('submitActivityBtn');
568
- const toastContainer = document.getElementById('toastContainer');
569
-
570
- let chatHistory = [];
571
- let recognition;
572
- let isRecording = false;
573
- let currentActivityPrompt = "";
574
- let currentUser = null;
575
-
576
- // --- SISTEMA DE TOAST NOTIFICATIONS ---
577
- function showToast(message, type = 'info', duration = 5000) {
578
- const toast = document.createElement('div');
579
- toast.className = `toast ${type}`;
580
- toast.innerHTML = `
581
- <button class="toast-close" onclick="this.parentElement.remove()">&times;</button>
582
- <div class="font-medium">${getToastTitle(type)}</div>
583
- <div class="text-sm mt-1">${message}</div>
584
- `;
585
-
586
- toastContainer.appendChild(toast);
587
-
588
- // Show animation
589
- setTimeout(() => toast.classList.add('show'), 100);
590
-
591
- // Auto remove
592
- setTimeout(() => {
593
- toast.classList.remove('show');
594
- setTimeout(() => toast.remove(), 300);
595
- }, duration);
596
- }
597
-
598
- function getToastTitle(type) {
599
- const titles = {
600
- 'success': '✅ Success',
601
- 'error': '❌ Error',
602
- 'warning': '⚠️ Warning',
603
- 'info': 'ℹ️ Info'
604
- };
605
- return titles[type] || 'Info';
606
- }
607
-
608
- // --- ENHANCED ERROR HANDLING ---
609
- async function handleApiRequest(url, options, errorContext = 'Request') {
610
- try {
611
- const response = await fetch(url, options);
612
-
613
- if (!response.ok) {
614
- let errorMessage = `HTTP ${response.status}`;
615
- try {
616
- const errorData = await response.json();
617
- errorMessage = errorData.error || errorMessage;
618
- } catch (e) {
619
- // Use default HTTP error
620
- }
621
- throw new Error(errorMessage);
622
- }
623
-
624
- return await response.json();
625
- } catch (error) {
626
- console.error(`${errorContext} failed:`, error);
627
-
628
- if (error.name === 'TypeError' && error.message.includes('fetch')) {
629
- showToast('Network connection error. Please check your internet connection.', 'error');
630
- } else if (error.message.includes('503')) {
631
- showToast('Service temporarily unavailable. Please check API configuration.', 'warning');
632
- } else {
633
- showToast(`${errorContext} failed: ${error.message}`, 'error');
634
- }
635
-
636
- throw error;
637
- }
638
- }
639
-
640
- // --- LOADING STATE MANAGEMENT ---
641
- function setButtonLoading(button, loading, originalText = null) {
642
- if (loading) {
643
- button.disabled = true;
644
- button.dataset.originalText = button.innerHTML;
645
- button.innerHTML = '<div class="spinner mr-2"></div> Loading...';
646
- } else {
647
- button.disabled = false;
648
- button.innerHTML = originalText || button.dataset.originalText || button.innerHTML;
649
- }
650
- }
651
-
652
- // --- INPUT VALIDATION ---
653
- function validateInput(input, errorMessage) {
654
- const value = input.value?.trim() || '';
655
- const errorElement = input.parentNode.querySelector('.error-message');
656
-
657
- if (!value) {
658
- input.classList.add('form-error');
659
- if (!errorElement) {
660
- const error = document.createElement('div');
661
- error.className = 'error-message';
662
- error.textContent = errorMessage;
663
- input.parentNode.appendChild(error);
664
- }
665
- return false;
666
- } else {
667
- input.classList.remove('form-error');
668
- if (errorElement) errorElement.remove();
669
- return true;
670
- }
671
- }
672
-
673
- // --- CARREGAMENTO DINÂMICO DE MODELOS ---
674
- async function populateModelSelector() {
675
- try {
676
- const models = await handleApiRequest('/list-models', {}, 'Loading models');
677
- modelSelector.innerHTML = '';
678
-
679
- const geminiGroup = document.createElement('optgroup');
680
- geminiGroup.label = 'Google Gemini';
681
- const groqGroup = document.createElement('optgroup');
682
- groqGroup.label = 'Groq (Ultra Fast)';
683
-
684
- models.forEach(model => {
685
- const option = document.createElement('option');
686
- option.value = model.value;
687
- option.textContent = model.name;
688
- if (model.value.startsWith('gemini:')) {
689
- geminiGroup.appendChild(option);
690
- } else if (model.value.startsWith('groq:')) {
691
- groqGroup.appendChild(option);
692
- }
693
- });
694
-
695
- modelSelector.appendChild(geminiGroup);
696
- modelSelector.appendChild(groqGroup);
697
- showToast('Models loaded successfully', 'success', 2000);
698
- } catch (error) {
699
- modelSelector.innerHTML = '<option>Error loading models</option>';
700
- }
701
- }
702
-
703
- // --- FUNÇÕES DE ÁUDIO ---
704
- async function fetchAudioBlob(text) {
705
- const selectedTld = voiceSelector.value;
706
- const response = await fetch('/tts-proxy', {
707
- method: 'POST',
708
- headers: { 'Content-Type': 'application/json' },
709
- body: JSON.stringify({ text: text, tld: selectedTld })
710
- });
711
- if (!response.ok) {
712
- const err = await response.json();
713
- throw new Error(err.error || `HTTP error! status: ${response.status}`);
714
- }
715
- return await response.blob();
716
- }
717
-
718
- async function playAudio(text, event) {
719
- if(event) event.stopPropagation();
720
- if (!text || !text.trim()) {
721
- showToast('No text to play', 'warning', 2000);
722
- return;
723
- }
724
-
725
- try {
726
- const audioBlob = await fetchAudioBlob(text);
727
- const audioUrl = URL.createObjectURL(audioBlob);
728
- const audio = new Audio(audioUrl);
729
-
730
- audio.onplay = () => showToast('Playing audio...', 'info', 1000);
731
- audio.onended = () => URL.revokeObjectURL(audioUrl);
732
- audio.onerror = () => showToast('Failed to play audio', 'error');
733
-
734
- await audio.play();
735
- } catch (error) {
736
- console.error('Error playing audio:', error);
737
- showToast('Failed to generate audio. Please try again.', 'error');
738
- }
739
- }
740
-
741
- // --- LÓGICA DO EDITOR ---
742
- const charCount = document.getElementById('charCount');
743
- const playText = document.getElementById('playText');
744
- const playSpinner = document.getElementById('playSpinner');
745
- textEditor.addEventListener('input', () => {
746
- const len = textEditor.innerText.length;
747
- charCount.textContent = `${len}/10000`;
748
- playButton.disabled = len === 0;
749
- });
750
- textEditor.addEventListener('mouseup', () => {
751
- createCardButton.disabled = !window.getSelection().toString().trim();
752
- });
753
- async function handlePlay() {
754
- const text = textEditor.innerText.trim();
755
- if (!text) {
756
- showToast('Please enter some text first', 'warning');
757
- return;
758
- }
759
-
760
- if (text.length > 5000) {
761
- showToast('Text is too long for audio generation. Please use shorter text.', 'warning');
762
- return;
763
- }
764
-
765
- setButtonLoading(playButton, true);
766
- playText.textContent = 'Generating...';
767
- playSpinner.classList.remove('hidden');
768
-
769
- try {
770
- const audioBlob = await fetchAudioBlob(text);
771
- const audioUrl = URL.createObjectURL(audioBlob);
772
- audioPlayer.src = audioUrl;
773
- audioPlayer.classList.remove('hidden');
774
-
775
- audioPlayer.onplay = () => showToast('Audio ready! Playing...', 'success', 1000);
776
- audioPlayer.onended = () => URL.revokeObjectURL(audioUrl);
777
- audioPlayer.onerror = () => showToast('Failed to play audio', 'error');
778
-
779
- await audioPlayer.play();
780
- } catch (error) {
781
- showToast('Failed to generate audio. Please check your connection and try again.', 'error');
782
- } finally {
783
- setButtonLoading(playButton, false);
784
- playText.textContent = 'Listen to Text';
785
- playSpinner.classList.add('hidden');
786
- }
787
- }
788
-
789
- // --- TODAS AS OUTRAS FUNÇÕES ---
790
-
791
- // ATIVIDADES
792
- async function generateActivity() {
793
- const activityType = activityTypeSelector.value;
794
- const context = contextSelector.value;
795
- const sourceText = textEditor.innerText;
796
- generateActivityButton.disabled = true;
797
- generateActivityButton.innerHTML = '<div class="spinner mr-2"></div> Generating...';
798
- activityChatDisplay.innerHTML = '<p class="text-gray-500 italic">Generating your exercise...</p>';
799
- activityResponseArea.classList.add('hidden');
800
- let prompt = "";
801
- if (activityType === 'translate_pt_en') {
802
- prompt = `Based on the '${context}' context, create one single sentence in Portuguese for me to translate into English.`;
803
- } else if (activityType === 'email_writing') {
804
- prompt = `Create a scenario for me to write a formal email, related to the '${context}' context. Give me clear instructions on what to write.`;
805
- } else if (activityType === 'summary_writing') {
806
- if (!sourceText) {
807
- alert("Please paste text in the main editor to generate a summary activity.");
808
- activityChatDisplay.innerHTML = '<p class="text-red-500">Please paste text in the editor in card #1 first.</p>';
809
- generateActivityButton.disabled = false;
810
- generateActivityButton.textContent = 'Generate Activity';
811
- return;
812
- }
813
- prompt = `Your task is to summarize the following text in 2 or 3 sentences: "${sourceText}"`;
814
- } else if (activityType === 'professional_conversation') {
815
- prompt = `Start a professional role-playing conversation with me related to '${context}'. Ask me an open-ended question to begin.`;
816
- }
817
- try {
818
- const response = await fetch('/explain-proxy', {
819
- method: 'POST',
820
- headers: { 'Content-Type': 'application/json' },
821
- body: JSON.stringify({
822
- model: modelSelector.value,
823
- context_focus: context,
824
- custom_prompt: prompt
825
- })
826
- });
827
- if (!response.ok) throw new Error((await response.json()).error);
828
- const data = await response.json();
829
- currentActivityPrompt = data.explanation;
830
- activityChatDisplay.innerHTML = `<div class="ai-prompt"><strong>Your Task:</strong><p>${currentActivityPrompt}</p></div>`;
831
- activityResponseArea.classList.remove('hidden');
832
- } catch (error) {
833
- activityChatDisplay.innerHTML = `<p class="text-red-600 font-semibold">Failed to generate activity: ${error.message}</p>`;
834
- } finally {
835
- generateActivityButton.disabled = false;
836
- generateActivityButton.textContent = 'Generate Activity';
837
- }
838
- }
839
- async function submitActivityResponse() {
840
- const userResponse = activityInput.value.trim();
841
- if (!userResponse) {
842
- alert("Please write a response.");
843
- return;
844
- }
845
- submitActivityBtn.disabled = true;
846
- submitActivityBtn.innerHTML = '<div class="spinner mr-2"></div> Getting Feedback...';
847
- const oldFeedback = activityChatDisplay.querySelector('.ai-feedback');
848
- if(oldFeedback) oldFeedback.remove();
849
- const oldResponse = activityChatDisplay.querySelector('.user-response-display');
850
- if(oldResponse) oldResponse.remove();
851
- const userResponseDiv = document.createElement('div');
852
- userResponseDiv.className = 'user-response-display';
853
- userResponseDiv.innerHTML = `<strong>Your Response:</strong><p>${userResponse}</p>`;
854
- activityChatDisplay.appendChild(userResponseDiv);
855
- try {
856
- const response = await fetch('/activity-feedback', {
857
- method: 'POST',
858
- headers: { 'Content-Type': 'application/json' },
859
- body: JSON.stringify({
860
- model: modelSelector.value,
861
- context_focus: contextSelector.value,
862
- original_prompt: currentActivityPrompt,
863
- user_response: userResponse
864
- })
865
- });
866
- if (!response.ok) throw new Error((await response.json()).error);
867
- const data = await response.json();
868
- const feedbackDiv = document.createElement('div');
869
- feedbackDiv.className = 'ai-feedback';
870
- feedbackDiv.innerHTML = `<strong>Feedback:</strong><div class="prose prose-sm max-w-none">${marked.parse(data.feedback)}</div>`;
871
- activityChatDisplay.appendChild(feedbackDiv);
872
- activityInput.value = '';
873
- } catch (error) {
874
- const errorDiv = document.createElement('div');
875
- errorDiv.className = 'ai-feedback';
876
- errorDiv.innerHTML = `<p class="text-red-600 font-semibold">Failed to get feedback: ${error.message}</p>`;
877
- activityChatDisplay.appendChild(errorDiv);
878
- } finally {
879
- submitActivityBtn.disabled = false;
880
- submitActivityBtn.textContent = 'Submit Answer';
881
- }
882
- }
883
-
884
- // CONVERSAÇÃO
885
- function initializeSpeechRecognition() {
886
- window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
887
- if (!window.SpeechRecognition) {
888
- micButton.disabled = true;
889
- micButton.textContent = '❌';
890
- alert('Voice recognition API is not supported in this browser.');
891
- return;
892
- }
893
- recognition = new SpeechRecognition();
894
- recognition.lang = 'en-US';
895
- recognition.interimResults = false;
896
- recognition.continuous = false;
897
- recognition.onresult = (event) => {
898
- const transcript = event.results[0][0].transcript;
899
- chatInput.value = transcript;
900
- sendMessage(transcript);
901
- };
902
- recognition.onerror = (event) => { console.error('Voice recognition error:', event.error); };
903
- recognition.onend = () => {
904
- isRecording = false;
905
- micButton.classList.remove('is-recording');
906
- micButton.innerHTML = '🎙️';
907
- };
908
- }
909
- function handleVoiceInput() {
910
- if (isRecording) {
911
- recognition.stop();
912
- } else {
913
- isRecording = true;
914
- micButton.classList.add('is-recording');
915
- micButton.innerHTML = '<div class="spinner"></div>';
916
- recognition.start();
917
- }
918
- }
919
- function handleTextInput() {
920
- const message = chatInput.value.trim();
921
- sendMessage(message);
922
- chatInput.value = '';
923
- }
924
- chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleTextInput(); });
925
- async function sendMessage(message) {
926
- if (!message) return;
927
- appendMessage(message, 'user');
928
- chatHistory.push({ role: 'user', content: message });
929
- try {
930
- const response = await fetch('/chat-with-ai', {
931
- method: 'POST',
932
- headers: { 'Content-Type': 'application/json' },
933
- body: JSON.stringify({ history: chatHistory, message: message })
934
- });
935
- if (!response.ok) throw new Error((await response.json()).error);
936
- const data = await response.json();
937
- appendMessage(data.response, 'ai');
938
- chatHistory.push({ role: 'assistant', content: data.response });
939
- playAudio(data.response);
940
- } catch (error) {
941
- appendMessage(`Chat error: ${error.message}`, 'ai');
942
- }
943
- }
944
- function appendMessage(text, sender) {
945
- const msg = document.createElement('div');
946
- msg.className = `chat-message ${sender}-message`;
947
- msg.textContent = text;
948
- chatDisplay.appendChild(msg);
949
- chatDisplay.scrollTop = chatDisplay.scrollHeight;
950
- }
951
-
952
- // IMAGEM
953
- imageUploader.addEventListener('change', (event) => {
954
- const file = event.target.files[0];
955
- if (!file) return;
956
- const reader = new FileReader();
957
- reader.onload = (e) => {
958
- imagePreview.src = e.target.result;
959
- imagePreview.classList.remove('hidden');
960
- analyzeImage(e.target.result);
961
- };
962
- reader.readAsDataURL(file);
963
- });
964
- async function analyzeImage(base64Image) {
965
- // Show skeleton loading
966
- imageAnalysisResult.innerHTML = `
967
- <div class="spinner-container">
968
- <div class="spinner spinner-large !border-l-indigo-500 !border-gray-200"></div>
969
- <p class="ml-3 text-gray-600">Analyzing image with AI...</p>
970
- </div>
971
- <div class="mt-4">
972
- <div class="skeleton-text loading-skeleton mb-2"></div>
973
- <div class="flex gap-2">
974
- <div class="skeleton-box loading-skeleton" style="width: 80px; height: 32px;"></div>
975
- <div class="skeleton-box loading-skeleton" style="width: 100px; height: 32px;"></div>
976
- <div class="skeleton-box loading-skeleton" style="width: 90px; height: 32px;"></div>
977
- </div>
978
- </div>`;
979
-
980
- try {
981
- const vocabulary = await handleApiRequest('/analyze-image', {
982
- method: 'POST',
983
- headers: { 'Content-Type': 'application/json' },
984
- body: JSON.stringify({
985
- image: base64Image,
986
- model: modelSelector.value
987
- })
988
- }, 'Analyzing image');
989
-
990
- displayImageVocabulary(vocabulary);
991
- showToast(`Found ${vocabulary.length} vocabulary items in the image!`, 'success');
992
- } catch (error) {
993
- imageAnalysisResult.innerHTML = `
994
- <div class="empty-state">
995
- <div class="empty-state-icon">❌</div>
996
- <div class="empty-state-title">Analysis Failed</div>
997
- <div class="empty-state-description">Please try uploading the image again or check your connection.</div>
998
- </div>`;
999
- }
1000
- }
1001
- function displayImageVocabulary(vocabulary) {
1002
- imageAnalysisResult.innerHTML = `
1003
- <div class="fade-in">
1004
- <h3 class="font-semibold mb-3 text-gray-700">🎯 Suggested Vocabulary (${vocabulary.length} items)</h3>
1005
- <div class="flex flex-wrap gap-2" id="vocabularyButtons"></div>
1006
- <p class="text-xs text-gray-500 mt-3">Click any term to create a flashcard</p>
1007
- </div>`;
1008
-
1009
- const buttonContainer = document.getElementById('vocabularyButtons');
1010
- vocabulary.forEach(({ term, definition }, index) => {
1011
- const btn = document.createElement('button');
1012
- btn.className = 'btn bg-violet-100 text-violet-700 hover:bg-violet-200 text-sm slide-in';
1013
- btn.style.animationDelay = `${index * 0.1}s`;
1014
- btn.innerHTML = `${term} <span class="text-violet-500 ml-1">+</span>`;
1015
- btn.title = `${term}: ${definition}`;
1016
- btn.onclick = () => {
1017
- btn.disabled = true;
1018
- btn.innerHTML = '<div class="spinner mr-1"></div> Creating...';
1019
- createFlashcardFromSuggestion(term, definition);
1020
- };
1021
- buttonContainer.appendChild(btn);
1022
- });
1023
- }
1024
- async function generateImage() {
1025
- const prompt = imagePromptInput.value.trim();
1026
- if (!prompt) {
1027
- alert('Please enter a description for the image.');
1028
- return;
1029
- }
1030
- generateImageBtn.disabled = true;
1031
- generateImageBtn.innerHTML = '<div class="spinner mr-2"></div> Generating...';
1032
- 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>`;
1033
- try {
1034
- const response = await fetch('/generate-image', {
1035
- method: 'POST',
1036
- headers: { 'Content-Type': 'application/json' },
1037
- body: JSON.stringify({ prompt: prompt })
1038
- });
1039
- if (!response.ok) throw new Error((await response.json()).error);
1040
- const data = await response.json();
1041
- imageResultContainer.innerHTML = `<img src="data:image/png;base64,${data.image_base64}" class="rounded-lg max-h-60 mx-auto" alt="AI-generated image"/>`;
1042
- } catch (error) {
1043
- imageResultContainer.innerHTML = `<p class="text-red-600 font-semibold">Failed to generate image: ${error.message}</p>`;
1044
- } finally {
1045
- generateImageBtn.disabled = false;
1046
- generateImageBtn.textContent = 'Generate';
1047
- }
1048
- }
1049
-
1050
- // FLASHCARDS
1051
- function createSkeletonFlashcard() {
1052
- const cardId = `skeleton-${Date.now()}`;
1053
- const container = document.createElement('div');
1054
- container.className = 'flashcard-container fade-in';
1055
- container.id = cardId;
1056
- container.innerHTML = `
1057
- <div class="flashcard-inner">
1058
- <div class="flashcard-front">
1059
- <div>
1060
- <div class="skeleton-text small loading-skeleton"></div>
1061
- <div class="skeleton-text large loading-skeleton mt-4"></div>
1062
- <div class="skeleton-text loading-skeleton"></div>
1063
- </div>
1064
- <div class="border-t pt-3 mt-3">
1065
- <div class="skeleton-text loading-skeleton"></div>
1066
- <div class="skeleton-text small loading-skeleton mt-2"></div>
1067
- </div>
1068
- </div>
1069
- </div>`;
1070
- return container;
1071
- }
1072
-
1073
- async function createFlashcard(word, context, definition = null) {
1074
- if (!word || !word.trim()) {
1075
- showToast('Please select a word first', 'warning');
1076
- return;
1077
- }
1078
-
1079
- // Hide empty state and show skeleton
1080
- noCardsMessage.classList.add('hidden');
1081
- const skeletonCard = createSkeletonFlashcard();
1082
- flashcardList.prepend(skeletonCard);
1083
-
1084
- const btn = definition ? null : createCardButton;
1085
- if (btn) setButtonLoading(btn, true);
1086
-
1087
- try {
1088
- const cardData = await handleApiRequest('/explain-proxy', {
1089
- method: 'POST',
1090
- headers: { 'Content-Type': 'application/json' },
1091
- body: JSON.stringify({
1092
- word: word.trim(),
1093
- context: context || 'General context',
1094
- for_flashcard: true,
1095
- model: modelSelector.value,
1096
- context_focus: contextSelector.value
1097
- })
1098
- }, 'Creating flashcard');
1099
-
1100
- if(definition && !cardData.definition) cardData.definition = definition;
1101
-
1102
- // Remove skeleton and add real card
1103
- skeletonCard.remove();
1104
- renderFlashcard(cardData);
1105
-
1106
- // Show appropriate message based on auth status
1107
- if (currentUser) {
1108
- showToast(`Flashcard for "${word}" created and saved to your account!`, 'success');
1109
- } else {
1110
- showToast(`Flashcard for "${word}" created! Login to save your progress.`, 'success');
1111
- }
1112
- } catch (error) {
1113
- // Remove skeleton on error
1114
- skeletonCard.remove();
1115
- // Show empty state if no cards left
1116
- if (flashcardList.children.length === 1) {
1117
- noCardsMessage.classList.remove('hidden');
1118
- }
1119
- } finally {
1120
- if (btn) setButtonLoading(btn, false);
1121
- }
1122
- }
1123
- function createFlashcardFromSelection() {
1124
- const selection = window.getSelection().toString().trim();
1125
- if (selection) createFlashcard(selection, textEditor.innerText);
1126
- }
1127
- function createFlashcardFromSuggestion(term, definition) {
1128
- createFlashcard(term, `(From image analysis) A visual representation of ${term}.`, definition);
1129
- }
1130
- function renderFlashcard(data) {
1131
- noCardsMessage.classList.add('hidden');
1132
- const cardId = `card-${Date.now()}`;
1133
- const container = document.createElement('div');
1134
- container.className = 'flashcard-container';
1135
- container.id = cardId;
1136
-
1137
- // Calculate dynamic height based on content length
1138
- const totalLength = (data.gapped_sentence?.length || 0) +
1139
- (data.translation?.length || 0) +
1140
- (data.definition?.length || 0) +
1141
- (data.context_sentence?.length || 0);
1142
-
1143
- let minHeight = 280;
1144
- if (totalLength > 300) minHeight = 350;
1145
- if (totalLength > 500) minHeight = 420;
1146
- if (totalLength > 800) minHeight = 500;
1147
-
1148
- // Responsive height adjustment
1149
- if (window.innerWidth <= 768) {
1150
- minHeight = Math.max(250, minHeight * 0.85);
1151
- }
1152
- if (window.innerWidth <= 480) {
1153
- minHeight = Math.max(220, minHeight * 0.75);
1154
- }
1155
-
1156
- container.style.minHeight = `${minHeight}px`;
1157
- const fallback = 'Info not generated';
1158
- container.innerHTML = `
1159
- <button class="delete-btn" onclick="deleteFlashcard('${cardId}', event)" title="Delete flashcard">🗑️</button>
1160
- <div class="flashcard-inner">
1161
- <div class="flashcard-front">
1162
- <div>
1163
- <div class="text-sm text-gray-600 mb-2">Context Sentence:</div>
1164
- <div class="flashcard-text text-lg text-center text-gray-800">${data.gapped_sentence || fallback}</div>
1165
- </div>
1166
- <div class="border-t pt-3 mt-3 text-center">
1167
- <strong class="text-indigo-600">Hint (PT):</strong>
1168
- <div class="flashcard-text mt-1">${data.translation || fallback}</div>
1169
- </div>
1170
- <div class="text-xs text-center text-gray-400 mt-4">👆 Click to see the answer</div>
1171
- </div>
1172
- <div class="flashcard-back">
1173
- <div>
1174
- <h3 class="text-xl font-bold text-center text-indigo-600 mb-2">${data.term || 'Term'}</h3>
1175
- <div class="flex items-center justify-center gap-2 text-gray-600 italic mb-3">
1176
- <button class="audio-btn" onclick="playAudio('${(data.context_sentence || '').replace(/'/g, "\\'")}', event)" title="Listen to pronunciation">🔊</button>
1177
- <span class="flashcard-text">${data.context_sentence || fallback}</span>
1178
- </div>
1179
- <div class="text-sm text-gray-800"><strong class="font-semibold">Definition:</strong>
1180
- <div class="flashcard-text mt-1">${data.definition || fallback}</div>
1181
- </div>
1182
- </div>
1183
- <div class="border-t mt-3 pt-3">
1184
- <h4 class="text-sm font-semibold text-center">Practice Your Pronunciation</h4>
1185
- <div class="flex justify-center items-center gap-2 mt-2">
1186
- <button class="btn btn-primary w-12 h-12 rounded-full mic-button" onclick="handlePronunciationPractice(this, '${(data.context_sentence || '').replace(/'/g, "\\'")}', event)" title="Record your pronunciation">🎙️</button>
1187
- </div>
1188
- <div class="text-xs text-gray-600 p-2 mt-2 bg-gray-50 rounded-md min-h-[40px] flashcard-text" data-feedback-area>Click the microphone to practice pronunciation</div>
1189
- </div>
1190
- </div>
1191
- </div>`;
1192
- container.classList.add('fade-in');
1193
- flashcardList.prepend(container);
1194
-
1195
- container.addEventListener('click', (e) => {
1196
- if(e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
1197
- container.classList.toggle('flipped');
1198
- }
1199
- });
1200
- }
1201
- function deleteFlashcard(cardId, event) {
1202
- event.stopPropagation();
1203
- const cardToDelete = document.getElementById(cardId);
1204
- if (cardToDelete) {
1205
- cardToDelete.remove();
1206
- }
1207
- if (flashcardList.children.length === 1) {
1208
- noCardsMessage.classList.remove('hidden');
1209
- }
1210
- }
1211
- async function handlePronunciationPractice(button, targetText, event) {
1212
- event.stopPropagation();
1213
- const feedbackArea = button.closest('.flashcard-back').querySelector('[data-feedback-area]');
1214
- const practiceRecognition = new SpeechRecognition();
1215
- practiceRecognition.lang = 'en-US';
1216
- button.innerHTML = '<div class="spinner"></div>';
1217
- button.disabled = true;
1218
- feedbackArea.textContent = 'Listening...';
1219
- practiceRecognition.start();
1220
- practiceRecognition.onresult = async (e) => {
1221
- const userText = e.results[0][0].transcript;
1222
- feedbackArea.textContent = `You said: "${userText}". Analyzing...`;
1223
- try {
1224
- const response = await fetch('/pronunciation-feedback', {
1225
- method: 'POST',
1226
- headers: { 'Content-Type': 'application/json' },
1227
- body: JSON.stringify({ target_text: targetText, user_text: userText })
1228
- });
1229
- if (!response.ok) throw new Error((await response.json()).error);
1230
- const data = await response.json();
1231
- feedbackArea.innerHTML = `<strong>Feedback:</strong> ${data.feedback}`;
1232
- } catch (error) {
1233
- feedbackArea.textContent = `Analysis error: ${error.message}`;
1234
- } finally {
1235
- button.innerHTML = '🎙️';
1236
- button.disabled = false;
1237
- }
1238
- };
1239
- practiceRecognition.onerror = (e) => {
1240
- feedbackArea.textContent = `Recording error: ${e.error}`;
1241
- button.innerHTML = '🎙️';
1242
- button.disabled = false;
1243
- };
1244
- }
1245
-
1246
- // --- AUTHENTICATION FUNCTIONS ---
1247
-
1248
- async function checkAuthStatus() {
1249
- try {
1250
- const response = await fetch('/auth/check');
1251
- const data = await response.json();
1252
-
1253
- if (data.authenticated) {
1254
- currentUser = data.user;
1255
- showAuthenticatedState();
1256
- // Load user settings into selectors
1257
- if (data.user.settings) {
1258
- loadUserSettings(data.user.settings);
1259
- }
1260
- } else {
1261
- showGuestState();
1262
- }
1263
- } catch (error) {
1264
- console.error('Auth check failed:', error);
1265
- showGuestState();
1266
- }
1267
- }
1268
-
1269
- function showAuthenticatedState() {
1270
- document.getElementById('guestAuth').style.display = 'none';
1271
- document.getElementById('userAuth').style.display = 'block';
1272
- document.getElementById('userEmail').textContent = currentUser.email;
1273
- document.getElementById('userAvatar').textContent = currentUser.email.charAt(0).toUpperCase();
1274
-
1275
- // Update session status
1276
- document.getElementById('guestStatus').style.display = 'none';
1277
- document.getElementById('userStatus').style.display = 'inline';
1278
- document.getElementById('loadFlashcardsBtn').style.display = 'inline-block';
1279
-
1280
- // Show advanced panels
1281
- document.getElementById('contentCuration').style.display = 'block';
1282
- document.getElementById('contentCurationLogin').style.display = 'none';
1283
- document.getElementById('analyticsPanel').style.display = 'block';
1284
- document.getElementById('studyPlannerPanel').style.display = 'block';
1285
-
1286
- // Load analytics and study plan
1287
- loadAnalytics();
1288
- loadCurrentStudyPlan();
1289
-
1290
- showToast(`Welcome back, ${currentUser.email}!`, 'success', 3000);
1291
- }
1292
-
1293
- function showGuestState() {
1294
- document.getElementById('guestAuth').style.display = 'flex';
1295
- document.getElementById('userAuth').style.display = 'none';
1296
-
1297
- // Update session status
1298
- document.getElementById('guestStatus').style.display = 'inline';
1299
- document.getElementById('userStatus').style.display = 'none';
1300
- document.getElementById('loadFlashcardsBtn').style.display = 'none';
1301
-
1302
- // Hide advanced panels
1303
- document.getElementById('contentCuration').style.display = 'none';
1304
- document.getElementById('contentCurationLogin').style.display = 'block';
1305
- document.getElementById('analyticsPanel').style.display = 'none';
1306
- document.getElementById('studyPlannerPanel').style.display = 'none';
1307
-
1308
- currentUser = null;
1309
- }
1310
-
1311
- function loadUserSettings(settings) {
1312
- if (settings.preferred_model) modelSelector.value = settings.preferred_model;
1313
- if (settings.context_focus) contextSelector.value = settings.context_focus;
1314
- if (settings.voice_accent) voiceSelector.value = settings.voice_accent;
1315
- }
1316
-
1317
- function showAuthModal(type = 'login') {
1318
- document.getElementById('authModal').classList.add('show');
1319
- switchAuthForm(type);
1320
- }
1321
-
1322
- function hideAuthModal() {
1323
- document.getElementById('authModal').classList.remove('show');
1324
- // Clear forms
1325
- document.getElementById('loginForm').reset();
1326
- document.getElementById('registerForm').reset();
1327
- }
1328
-
1329
- function switchAuthForm(type) {
1330
- const loginForm = document.getElementById('loginForm');
1331
- const registerForm = document.getElementById('registerForm');
1332
- const title = document.getElementById('authModalTitle');
1333
-
1334
- if (type === 'login') {
1335
- loginForm.classList.add('active');
1336
- registerForm.classList.remove('active');
1337
- title.textContent = 'Login';
1338
- } else {
1339
- registerForm.classList.add('active');
1340
- loginForm.classList.remove('active');
1341
- title.textContent = 'Create Account';
1342
- }
1343
- }
1344
-
1345
- function toggleUserMenu() {
1346
- document.getElementById('userAuth').classList.toggle('show');
1347
- }
1348
-
1349
- // Close user menu when clicking outside
1350
- document.addEventListener('click', (e) => {
1351
- if (!e.target.closest('.user-menu')) {
1352
- document.getElementById('userAuth').classList.remove('show');
1353
- }
1354
- });
1355
-
1356
- // Login form submission
1357
- document.getElementById('loginForm').addEventListener('submit', async (e) => {
1358
- e.preventDefault();
1359
- const email = document.getElementById('loginEmail').value;
1360
- const password = document.getElementById('loginPassword').value;
1361
-
1362
- const submitBtn = e.target.querySelector('button[type="submit"]');
1363
- setButtonLoading(submitBtn, true);
1364
-
1365
- try {
1366
- const data = await handleApiRequest('/login', {
1367
- method: 'POST',
1368
- headers: { 'Content-Type': 'application/json' },
1369
- body: JSON.stringify({ email, password })
1370
- }, 'Logging in');
1371
-
1372
- currentUser = data.user;
1373
- hideAuthModal();
1374
- showAuthenticatedState();
1375
-
1376
- if (data.user.settings) {
1377
- loadUserSettings(data.user.settings);
1378
- }
1379
-
1380
- } catch (error) {
1381
- // Error already handled by handleApiRequest
1382
- } finally {
1383
- setButtonLoading(submitBtn, false);
1384
- }
1385
- });
1386
-
1387
- // Register form submission
1388
- document.getElementById('registerForm').addEventListener('submit', async (e) => {
1389
- e.preventDefault();
1390
- const email = document.getElementById('registerEmail').value;
1391
- const password = document.getElementById('registerPassword').value;
1392
- const confirmPassword = document.getElementById('confirmPassword').value;
1393
-
1394
- if (password !== confirmPassword) {
1395
- showToast('Passwords do not match', 'error');
1396
- return;
1397
- }
1398
-
1399
- const submitBtn = e.target.querySelector('button[type="submit"]');
1400
- setButtonLoading(submitBtn, true);
1401
-
1402
- try {
1403
- const data = await handleApiRequest('/register', {
1404
- method: 'POST',
1405
- headers: { 'Content-Type': 'application/json' },
1406
- body: JSON.stringify({ email, password })
1407
- }, 'Creating account');
1408
-
1409
- hideAuthModal();
1410
- showToast('Account created! Please check your email to confirm your account.', 'success', 8000);
1411
-
1412
- } catch (error) {
1413
- // Error already handled by handleApiRequest
1414
- } finally {
1415
- setButtonLoading(submitBtn, false);
1416
- }
1417
- });
1418
-
1419
- async function logout() {
1420
- try {
1421
- await handleApiRequest('/logout', {
1422
- method: 'POST'
1423
- }, 'Logging out');
1424
-
1425
- showGuestState();
1426
- showToast('Logged out successfully', 'info');
1427
-
1428
- } catch (error) {
1429
- // Error already handled by handleApiRequest
1430
- }
1431
- }
1432
-
1433
- async function showUserSettings() {
1434
- if (!currentUser) return;
1435
-
1436
- document.getElementById('userAuth').classList.remove('show');
1437
-
1438
- // Simple settings update using current form values
1439
- const settings = {
1440
- preferred_model: modelSelector.value,
1441
- context_focus: contextSelector.value,
1442
- voice_accent: voiceSelector.value,
1443
- daily_goal: 10, // Default
1444
- notification_enabled: true // Default
1445
- };
1446
-
1447
- try {
1448
- await handleApiRequest('/user/settings', {
1449
- method: 'POST',
1450
- headers: { 'Content-Type': 'application/json' },
1451
- body: JSON.stringify(settings)
1452
- }, 'Saving settings');
1453
-
1454
- showToast('Settings saved successfully!', 'success');
1455
- } catch (error) {
1456
- // Error already handled
1457
- }
1458
- }
1459
-
1460
- async function showUserFlashcards() {
1461
- if (!currentUser) return;
1462
-
1463
- document.getElementById('userAuth').classList.remove('show');
1464
-
1465
- try {
1466
- const data = await handleApiRequest('/user/flashcards', {}, 'Loading your flashcards');
1467
-
1468
- // Clear current flashcards
1469
- flashcardList.innerHTML = '';
1470
-
1471
- if (data.flashcards && data.flashcards.length > 0) {
1472
- data.flashcards.forEach(card => {
1473
- const flashcardData = {
1474
- term: card.term,
1475
- translation: card.translation,
1476
- context_sentence: card.context_sentence,
1477
- gapped_sentence: card.gapped_sentence,
1478
- definition: card.definition
1479
- };
1480
- renderFlashcard(flashcardData);
1481
- });
1482
- showToast(`Loaded ${data.flashcards.length} saved flashcards`, 'success');
1483
- } else {
1484
- noCardsMessage.classList.remove('hidden');
1485
- showToast('No saved flashcards found', 'info');
1486
- }
1487
-
1488
- } catch (error) {
1489
- noCardsMessage.classList.remove('hidden');
1490
- }
1491
- }
1492
-
1493
- // --- CONTENT CURATION FUNCTIONS ---
1494
-
1495
- async function searchContent() {
1496
- if (!currentUser) {
1497
- showToast('Please log in to search content', 'warning');
1498
- return;
1499
- }
1500
-
1501
- const query = document.getElementById('contentSearchInput').value.trim();
1502
- const category = document.getElementById('contentCategorySelect').value;
1503
-
1504
- if (!query) {
1505
- showToast('Please enter a search topic', 'warning');
1506
- return;
1507
- }
1508
-
1509
- const resultsContainer = document.getElementById('contentResults');
1510
- resultsContainer.innerHTML = '<div class="spinner-container"><div class="spinner spinner-large"></div><p class="ml-3">Searching for relevant content...</p></div>';
1511
-
1512
- try {
1513
- const data = await handleApiRequest('/content/search', {
1514
- method: 'POST',
1515
- headers: { 'Content-Type': 'application/json' },
1516
- body: JSON.stringify({ query, category })
1517
- }, 'Searching content');
1518
-
1519
- displayContentResults(data.results);
1520
- } catch (error) {
1521
- resultsContainer.innerHTML = '<div class="text-red-600">Failed to search content. Please try again.</div>';
1522
- }
1523
- }
1524
-
1525
- async function getRecommendations() {
1526
- if (!currentUser) {
1527
- showToast('Please log in to get recommendations', 'warning');
1528
- return;
1529
- }
1530
-
1531
- const resultsContainer = document.getElementById('contentResults');
1532
- resultsContainer.innerHTML = '<div class="spinner-container"><div class="spinner spinner-large"></div><p class="ml-3">Generating AI recommendations...</p></div>';
1533
-
1534
- try {
1535
- const data = await handleApiRequest('/content/recommendations', {}, 'Getting recommendations');
1536
- displayRecommendations(data.recommendations);
1537
- } catch (error) {
1538
- resultsContainer.innerHTML = '<div class="text-red-600">Failed to get recommendations. Please try again.</div>';
1539
- }
1540
- }
1541
-
1542
- function displayContentResults(results) {
1543
- const container = document.getElementById('contentResults');
1544
-
1545
- if (!results || results.length === 0) {
1546
- container.innerHTML = `
1547
- <div class="empty-state">
1548
- <div class="empty-state-icon">🔍</div>
1549
- <div class="empty-state-title">No Content Found</div>
1550
- <div class="empty-state-description">Try different search terms or check your interests settings</div>
1551
- </div>`;
1552
- return;
1553
- }
1554
-
1555
- container.innerHTML = results.map(result => `
1556
- <div class="content-item fade-in">
1557
- <div class="content-item-title">${result.title}</div>
1558
- <div class="content-item-meta">
1559
- <span class="difficulty-badge difficulty-${getDifficultyClass(result.estimated_difficulty || 3)}">
1560
- Level ${result.estimated_difficulty || 3}
1561
- </span>
1562
- <span class="ml-2">${result.source}</span>
1563
- ${result.published ? `<span class="ml-2">${new Date(result.published).toLocaleDateString()}</span>` : ''}
1564
- </div>
1565
- <div class="content-item-summary">${result.summary}</div>
1566
- <div class="flex gap-2 mt-3">
1567
- <button onclick="extractAndSaveContent('${result.url}', '${result.title.replace(/'/g, "\\'")}')" class="btn bg-green-500 hover:bg-green-600 text-white text-sm">📖 Read & Save</button>
1568
- <a href="${result.url}" target="_blank" class="btn bg-gray-500 hover:bg-gray-600 text-white text-sm">🔗 Open Original</a>
1569
- </div>
1570
- </div>
1571
- `).join('');
1572
- }
1573
-
1574
- function displayRecommendations(recommendations) {
1575
- const container = document.getElementById('contentResults');
1576
-
1577
- if (!recommendations || recommendations.length === 0) {
1578
- container.innerHTML = `
1579
- <div class="empty-state">
1580
- <div class="empty-state-icon">🤖</div>
1581
- <div class="empty-state-title">No Recommendations</div>
1582
- <div class="empty-state-description">Update your interests in settings to get better recommendations</div>
1583
- </div>`;
1584
- return;
1585
- }
1586
-
1587
- container.innerHTML = `
1588
- <h3 class="text-lg font-semibold mb-4 text-purple-600">✨ AI Recommendations for You</h3>
1589
- ` + recommendations.map(rec => `
1590
- <div class="content-item fade-in border-purple-200">
1591
- <div class="content-item-title text-purple-700">${rec.topic}</div>
1592
- <div class="content-item-meta">
1593
- <span class="difficulty-badge difficulty-${getDifficultyClass(rec.difficulty)}">
1594
- ${rec.difficulty}
1595
- </span>
1596
- <span class="ml-2">AI Recommended</span>
1597
- </div>
1598
- <div class="content-item-summary">${rec.reason}</div>
1599
- <div class="flex gap-2 mt-3">
1600
- <button onclick="searchSpecificTopic('${rec.topic.replace(/'/g, "\\'")}')" class="btn bg-purple-500 hover:bg-purple-600 text-white text-sm">🔍 Find Articles</button>
1601
- </div>
1602
- </div>
1603
- `).join('');
1604
- }
1605
-
1606
- function getDifficultyClass(level) {
1607
- if (level <= 2) return 'easy';
1608
- if (level <= 4) return 'medium';
1609
- return 'hard';
1610
- }
1611
-
1612
- async function extractAndSaveContent(url, title) {
1613
- try {
1614
- showToast('Extracting content...', 'info', 2000);
1615
-
1616
- const extractData = await handleApiRequest('/content/extract', {
1617
- method: 'POST',
1618
- headers: { 'Content-Type': 'application/json' },
1619
- body: JSON.stringify({ url })
1620
- }, 'Extracting content');
1621
-
1622
- if (extractData.success) {
1623
- const saveData = await handleApiRequest('/content/save', {
1624
- method: 'POST',
1625
- headers: { 'Content-Type': 'application/json' },
1626
- body: JSON.stringify({
1627
- title: extractData.title || title,
1628
- content: extractData.content,
1629
- source_url: url,
1630
- source_type: 'web_search'
1631
- })
1632
- }, 'Saving content');
1633
-
1634
- showToast('Content saved successfully!', 'success');
1635
- loadAnalytics(); // Refresh analytics
1636
- }
1637
- } catch (error) {
1638
- // Error already handled by handleApiRequest
1639
- }
1640
- }
1641
-
1642
- async function saveCustomContent() {
1643
- if (!currentUser) {
1644
- showToast('Please log in to save content', 'warning');
1645
- return;
1646
- }
1647
-
1648
- const title = document.getElementById('customContentTitle').value.trim();
1649
- const content = document.getElementById('customContentText').value.trim();
1650
- const url = document.getElementById('customContentUrl').value.trim();
1651
-
1652
- if (!title || !content) {
1653
- showToast('Please provide both title and content', 'warning');
1654
- return;
1655
- }
1656
-
1657
- try {
1658
- await handleApiRequest('/content/save', {
1659
- method: 'POST',
1660
- headers: { 'Content-Type': 'application/json' },
1661
- body: JSON.stringify({
1662
- title,
1663
- content,
1664
- source_url: url || null,
1665
- source_type: 'manual'
1666
- })
1667
- }, 'Saving content');
1668
-
1669
- // Clear form
1670
- document.getElementById('customContentTitle').value = '';
1671
- document.getElementById('customContentText').value = '';
1672
- document.getElementById('customContentUrl').value = '';
1673
-
1674
- showToast('Content saved successfully!', 'success');
1675
- loadAnalytics(); // Refresh analytics
1676
- } catch (error) {
1677
- // Error already handled
1678
- }
1679
- }
1680
-
1681
- async function showSavedArticles() {
1682
- if (!currentUser) return;
1683
-
1684
- try {
1685
- const data = await handleApiRequest('/content/articles', {}, 'Loading saved articles');
1686
-
1687
- const container = document.getElementById('contentResults');
1688
-
1689
- if (!data.articles || data.articles.length === 0) {
1690
- container.innerHTML = `
1691
- <div class="empty-state">
1692
- <div class="empty-state-icon">📚</div>
1693
- <div class="empty-state-title">No Saved Articles</div>
1694
- <div class="empty-state-description">Search and save articles to build your personal library</div>
1695
- </div>`;
1696
- return;
1697
- }
1698
-
1699
- container.innerHTML = `
1700
- <h3 class="text-lg font-semibold mb-4 text-green-600">📚 Your Saved Articles (${data.articles.length})</h3>
1701
- ` + data.articles.map(article => `
1702
- <div class="content-item fade-in">
1703
- <div class="content-item-title">${article.title}</div>
1704
- <div class="content-item-meta">
1705
- <span class="difficulty-badge difficulty-medium">${article.word_count} words</span>
1706
- <span class="ml-2">${new Date(article.created_at).toLocaleDateString()}</span>
1707
- ${article.category ? `<span class="ml-2">${article.category}</span>` : ''}
1708
- </div>
1709
- <div class="content-item-summary">${article.content.substring(0, 150)}...</div>
1710
- <div class="flex gap-2 mt-3">
1711
- <button onclick="loadArticleForStudy('${article.id}', '${article.title.replace(/'/g, "\\'")}')" class="btn btn-primary text-sm">📖 Study This</button>
1712
- ${article.source_url ? `<a href="${article.source_url}" target="_blank" class="btn bg-gray-500 hover:bg-gray-600 text-white text-sm">🔗 Original</a>` : ''}
1713
- </div>
1714
- </div>
1715
- `).join('');
1716
- } catch (error) {
1717
- // Error already handled
1718
- }
1719
- }
1720
-
1721
- function loadArticleForStudy(articleId, title) {
1722
- // Load the article content into the text editor
1723
- showToast(`Loading "${title}" for study...`, 'info');
1724
- // This would load the full article content - simplified for now
1725
- showToast('Article loaded! You can now create flashcards from it.', 'success');
1726
- }
1727
-
1728
- function searchSpecificTopic(topic) {
1729
- document.getElementById('contentSearchInput').value = topic;
1730
- searchContent();
1731
- }
1732
-
1733
- // --- ANALYTICS FUNCTIONS ---
1734
-
1735
- async function loadAnalytics() {
1736
- if (!currentUser) return;
1737
-
1738
- try {
1739
- const data = await handleApiRequest('/analytics/dashboard', {}, 'Loading analytics');
1740
-
1741
- document.getElementById('totalFlashcards').textContent = data.analytics.total_flashcards || 0;
1742
- document.getElementById('totalArticles').textContent = data.analytics.total_articles || 0;
1743
- document.getElementById('currentLevel').textContent = data.analytics.user_level || 'B1';
1744
- } catch (error) {
1745
- // Error already handled
1746
- }
1747
- }
1748
-
1749
- function toggleAnalytics() {
1750
- const content = document.getElementById('analyticsContent');
1751
- content.style.display = content.style.display === 'none' ? 'block' : 'none';
1752
- }
1753
-
1754
- // --- STUDY PLANNER FUNCTIONS ---
1755
-
1756
- async function generateStudyPlan() {
1757
- if (!currentUser) return;
1758
-
1759
- try {
1760
- const planData = {
1761
- current_level: currentUser.english_level || 'B1',
1762
- target_level: document.getElementById('targetLevelSelect')?.value || 'B2',
1763
- weekly_hours: parseInt(document.getElementById('weeklyHoursSelect')?.value || 5),
1764
- context_focus: currentUser.context_focus || 'General/Social',
1765
- study_goals: []
1766
- };
1767
-
1768
- showLoading('Generating your personalized study plan...');
1769
-
1770
- const response = await fetch('/study-plan/create', {
1771
- method: 'POST',
1772
- headers: {
1773
- 'Content-Type': 'application/json',
1774
- },
1775
- body: JSON.stringify(planData)
1776
- });
1777
-
1778
- const result = await response.json();
1779
- hideLoading();
1780
-
1781
- if (result.success) {
1782
- displayStudyPlan(result.plan);
1783
- showToast('Study plan generated successfully!', 'success');
1784
- } else {
1785
- showToast('Failed to generate study plan: ' + result.error, 'error');
1786
- }
1787
- } catch (error) {
1788
- hideLoading();
1789
- console.error('Error generating study plan:', error);
1790
- showToast('Error generating study plan', 'error');
1791
- }
1792
- }
1793
-
1794
- async function loadCurrentStudyPlan() {
1795
- if (!currentUser) return;
1796
-
1797
- try {
1798
- const response = await fetch('/study-plan/current');
1799
- const result = await response.json();
1800
-
1801
- if (result.success && result.plan) {
1802
- displayStudyPlan(result.plan);
1803
- loadStudyProgress();
1804
- }
1805
- } catch (error) {
1806
- console.error('Error loading study plan:', error);
1807
- }
1808
- }
1809
-
1810
- async function loadStudyProgress() {
1811
- if (!currentUser) return;
1812
-
1813
- try {
1814
- const response = await fetch('/study-plan/progress');
1815
- const result = await response.json();
1816
-
1817
- if (result.success && result.progress) {
1818
- displayStudyProgress(result.progress);
1819
- }
1820
- } catch (error) {
1821
- console.error('Error loading study progress:', error);
1822
- }
1823
- }
1824
-
1825
- async function completeActivity(planId, activityId, durationMinutes) {
1826
- if (!currentUser) return;
1827
-
1828
- try {
1829
- const notes = prompt('Any notes about this activity? (optional)') || '';
1830
-
1831
- const response = await fetch('/study-plan/activity/complete', {
1832
- method: 'POST',
1833
- headers: {
1834
- 'Content-Type': 'application/json',
1835
- },
1836
- body: JSON.stringify({
1837
- plan_id: planId,
1838
- activity_id: activityId,
1839
- duration_minutes: durationMinutes,
1840
- notes: notes
1841
- })
1842
- });
1843
-
1844
- const result = await response.json();
1845
-
1846
- if (result.success) {
1847
- showToast('Activity completed successfully!', 'success');
1848
-
1849
- // Update the UI
1850
- const activityCard = document.querySelector(`[data-activity-id="${activityId}"]`);
1851
- if (activityCard) {
1852
- activityCard.classList.add('completed');
1853
- const button = activityCard.querySelector('.complete-activity-btn');
1854
- if (button) {
1855
- button.textContent = '✅ Completed';
1856
- button.disabled = true;
1857
- }
1858
- }
1859
-
1860
- // Refresh progress
1861
- loadStudyProgress();
1862
- } else {
1863
- showToast('Failed to complete activity', 'error');
1864
- }
1865
- } catch (error) {
1866
- console.error('Error completing activity:', error);
1867
- showToast('Error completing activity', 'error');
1868
- }
1869
- }
1870
-
1871
- function displayStudyPlan(plan) {
1872
- const container = document.getElementById('studyPlanResult');
1873
- if (!container) return;
1874
-
1875
- container.innerHTML = `
1876
- <div class="study-plan-container">
1877
- <div class="study-plan-header">
1878
- <h3>📅 Your Personalized Study Plan</h3>
1879
- <div class="plan-overview grid grid-cols-2 gap-4 mt-4">
1880
- <div class="plan-stat">
1881
- <strong>Current Level:</strong> ${plan.current_level}
1882
- </div>
1883
- <div class="plan-stat">
1884
- <strong>Target Level:</strong> ${plan.target_level}
1885
- </div>
1886
- <div class="plan-stat">
1887
- <strong>Weekly Hours:</strong> ${plan.weekly_hours}h
1888
- </div>
1889
- <div class="plan-stat">
1890
- <strong>Duration:</strong> ${plan.estimated_weeks} weeks
1891
- </div>
1892
- </div>
1893
- </div>
1894
-
1895
- <div class="study-tips mt-6">
1896
- <h4 class="font-medium mb-2">💡 Personalized Study Tips</h4>
1897
- <ul class="space-y-1">
1898
- ${plan.study_tips.map(tip => `<li class="text-sm text-gray-600">${tip}</li>`).join('')}
1899
- </ul>
1900
- </div>
1901
-
1902
- <div class="weekly-activities mt-6">
1903
- <h4 class="font-medium mb-3">📚 Weekly Activities</h4>
1904
- <div class="activities-grid space-y-3">
1905
- ${plan.activities.slice(0, 6).map(activity => `
1906
- <div class="activity-card bg-gray-50 p-3 rounded-lg" data-activity-id="${activity.id}">
1907
- <div class="activity-header flex items-center gap-2 mb-2">
1908
- <span class="text-lg">${activity.icon}</span>
1909
- <strong class="text-sm">${activity.title}</strong>
1910
- </div>
1911
- <div class="activity-details text-xs text-gray-600">
1912
- <p><strong>Duration:</strong> ${activity.duration_minutes} minutes</p>
1913
- <p><strong>Day:</strong> ${['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][activity.day_of_week]}</p>
1914
- </div>
1915
- <button class="complete-activity-btn mt-2 bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600"
1916
- onclick="completeActivity('${plan.db_id || plan.id}', '${activity.id}', ${activity.duration_minutes})">
1917
- ✅ Mark Complete
1918
- </button>
1919
- </div>
1920
- `).join('')}
1921
- </div>
1922
- </div>
1923
-
1924
- <div class="milestones mt-6">
1925
- <h4 class="font-medium mb-3">🎯 Study Milestones</h4>
1926
- <div class="milestones-list space-y-2">
1927
- ${plan.milestones.slice(0, 3).map(milestone => `
1928
- <div class="milestone-item flex items-center gap-3 p-2 bg-blue-50 rounded ${milestone.completed ? 'completed opacity-75' : ''}">
1929
- <div class="milestone-week bg-blue-500 text-white px-2 py-1 rounded text-xs">Week ${milestone.week}</div>
1930
- <div class="milestone-content flex-1">
1931
- <strong class="text-sm">${milestone.title}</strong>
1932
- <p class="text-xs text-gray-600">${milestone.description}</p>
1933
- </div>
1934
- </div>
1935
- `).join('')}
1936
- </div>
1937
- </div>
1938
- </div>
1939
- `;
1940
-
1941
- container.classList.remove('hidden');
1942
- }
1943
-
1944
- function displayStudyProgress(progress) {
1945
- // This would be called to show progress in analytics section
1946
- console.log('Study progress:', progress);
1947
- }
1948
-
1949
- // --- INICIALIZAÇÃO ---
1950
- document.addEventListener('DOMContentLoaded', () => {
1951
- checkAuthStatus();
1952
- populateModelSelector();
1953
- initializeSpeechRecognition();
1954
- });
1955
- </script>
1956
-
1957
- </body>
1958
- </html>
1959
-