amauricunha commited on
Commit
d770486
·
verified ·
1 Parent(s): 91851cd

Upload 2 files

Browse files
Files changed (2) hide show
  1. templates/admin.html +1083 -0
  2. templates/index.html +1959 -1
templates/admin.html ADDED
@@ -0,0 +1,1083 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>English Helper - Admin Panel</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <style>
10
+ .admin-card {
11
+ @apply bg-white rounded-lg shadow-md p-6 border border-gray-200;
12
+ }
13
+ .stat-card {
14
+ @apply bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-lg p-4 shadow-md;
15
+ }
16
+ .danger-zone {
17
+ @apply bg-red-50 border border-red-200 rounded-lg p-4;
18
+ }
19
+ .success-zone {
20
+ @apply bg-green-50 border border-green-200 rounded-lg p-4;
21
+ }
22
+ .spinner {
23
+ border: 3px solid #f3f3f3;
24
+ border-top: 3px solid #3498db;
25
+ border-radius: 50%;
26
+ width: 20px;
27
+ height: 20px;
28
+ animation: spin 1s linear infinite;
29
+ display: inline-block;
30
+ margin-right: 10px;
31
+ }
32
+ @keyframes spin {
33
+ 0% { transform: rotate(0deg); }
34
+ 100% { transform: rotate(360deg); }
35
+ }
36
+ .table-container {
37
+ max-height: 600px;
38
+ overflow-y: auto;
39
+ }
40
+ </style>
41
+ </head>
42
+ <body class="bg-gray-100 min-h-screen">
43
+ <!-- Admin Login Modal -->
44
+ <div id="loginModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
45
+ <div class="bg-white rounded-lg p-8 max-w-md w-full mx-4">
46
+ <h2 class="text-2xl font-bold mb-6 text-center">🔐 Admin Login</h2>
47
+ <form id="adminLoginForm">
48
+ <div class="mb-4">
49
+ <label class="block text-gray-700 text-sm font-bold mb-2">Username</label>
50
+ <input type="text" id="adminUsername" class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500" required>
51
+ </div>
52
+ <div class="mb-6">
53
+ <label class="block text-gray-700 text-sm font-bold mb-2">Password</label>
54
+ <input type="password" id="adminPassword" class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500" required>
55
+ </div>
56
+ <button type="submit" class="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition duration-200">
57
+ 🚀 Login
58
+ </button>
59
+ </form>
60
+ <div id="loginError" class="mt-4 text-red-600 text-sm hidden"></div>
61
+ </div>
62
+ </div>
63
+
64
+ <!-- Admin Interface -->
65
+ <div id="adminInterface" class="hidden">
66
+ <!-- Header -->
67
+ <header class="bg-white shadow-md">
68
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
69
+ <div class="flex justify-between items-center py-4">
70
+ <div class="flex items-center">
71
+ <h1 class="text-2xl font-bold text-gray-900">🎓 English Helper Admin</h1>
72
+ </div>
73
+ <div class="flex items-center space-x-4">
74
+ <span id="adminUserInfo" class="text-gray-600"></span>
75
+ <button onclick="adminLogout()" class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600 transition duration-200">
76
+ 🚪 Logout
77
+ </button>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </header>
82
+
83
+ <!-- Navigation Tabs -->
84
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6">
85
+ <div class="border-b border-gray-200">
86
+ <nav class="-mb-px flex space-x-8">
87
+ <button onclick="showTab('dashboard')" class="admin-tab active" data-tab="dashboard">
88
+ 📊 Dashboard
89
+ </button>
90
+ <button onclick="showTab('users')" class="admin-tab" data-tab="users">
91
+ 👥 Users
92
+ </button>
93
+ <button onclick="showTab('database')" class="admin-tab" data-tab="database">
94
+ 🗄️ Database
95
+ </button>
96
+ <button onclick="showTab('tokens')" class="admin-tab" data-tab="tokens">
97
+ 🎯 Token Usage
98
+ </button>
99
+ <button onclick="showTab('system')" class="admin-tab" data-tab="system">
100
+ 🖥️ System Health
101
+ </button>
102
+ </nav>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- Tab Content -->
107
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
108
+ <!-- Dashboard Tab -->
109
+ <div id="dashboardTab" class="tab-content">
110
+ <h2 class="text-2xl font-bold mb-6">📊 System Dashboard</h2>
111
+
112
+ <!-- System Stats Cards -->
113
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
114
+ <div class="stat-card">
115
+ <h3 class="text-lg font-semibold mb-2">👥 Total Users</h3>
116
+ <p id="totalUsers" class="text-3xl font-bold">-</p>
117
+ <p class="text-sm opacity-80">
118
+ <span id="newUsersWeek">-</span> this week
119
+ </p>
120
+ </div>
121
+ <div class="stat-card">
122
+ <h3 class="text-lg font-semibold mb-2">📚 Study Sessions</h3>
123
+ <p id="totalSessions" class="text-3xl font-bold">-</p>
124
+ <p class="text-sm opacity-80">All time</p>
125
+ </div>
126
+ <div class="stat-card">
127
+ <h3 class="text-lg font-semibold mb-2">🎯 API Tokens</h3>
128
+ <p id="totalTokens" class="text-3xl font-bold">-</p>
129
+ <p class="text-sm opacity-80">
130
+ $<span id="estimatedCost">-</span> estimated cost
131
+ </p>
132
+ </div>
133
+ <div class="stat-card">
134
+ <h3 class="text-lg font-semibold mb-2">📝 Content Items</h3>
135
+ <p id="totalContent" class="text-3xl font-bold">-</p>
136
+ <p class="text-sm opacity-80">Articles & flashcards</p>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Charts Section -->
141
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
142
+ <div class="admin-card">
143
+ <h3 class="text-lg font-semibold mb-4">📈 User Growth</h3>
144
+ <canvas id="userGrowthChart" width="400" height="200"></canvas>
145
+ </div>
146
+ <div class="admin-card">
147
+ <h3 class="text-lg font-semibold mb-4">💰 Token Usage Costs</h3>
148
+ <canvas id="tokenCostChart" width="400" height="200"></canvas>
149
+ </div>
150
+ </div>
151
+
152
+ <!-- Recent Activity -->
153
+ <div class="admin-card">
154
+ <h3 class="text-lg font-semibold mb-4">🔄 Recent Activity</h3>
155
+ <div id="recentActivity" class="space-y-2">
156
+ <div class="text-center text-gray-500 py-4">Loading recent activity...</div>
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ <!-- Users Tab -->
162
+ <div id="usersTab" class="tab-content hidden">
163
+ <div class="flex justify-between items-center mb-6">
164
+ <h2 class="text-2xl font-bold">👥 User Management</h2>
165
+ <div class="flex items-center space-x-4">
166
+ <input type="text" id="userSearch" placeholder="Search users..." class="px-4 py-2 border rounded-lg">
167
+ <button onclick="refreshUsers()" class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
168
+ 🔄 Refresh
169
+ </button>
170
+ <button onclick="exportUsers()" class="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600">
171
+ 📥 Export CSV
172
+ </button>
173
+ </div>
174
+ </div>
175
+
176
+ <!-- Users Table -->
177
+ <div class="admin-card">
178
+ <div class="table-container">
179
+ <table class="w-full table-auto">
180
+ <thead class="bg-gray-50 sticky top-0">
181
+ <tr>
182
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
183
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
184
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Activity</th>
185
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
186
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
187
+ </tr>
188
+ </thead>
189
+ <tbody id="usersTableBody">
190
+ <tr>
191
+ <td colspan="5" class="text-center py-8">
192
+ <div class="spinner"></div> Loading users...
193
+ </td>
194
+ </tr>
195
+ </tbody>
196
+ </table>
197
+ </div>
198
+
199
+ <!-- Pagination -->
200
+ <div id="usersPagination" class="flex justify-between items-center mt-4 pt-4 border-t">
201
+ <div id="usersInfo" class="text-sm text-gray-600"></div>
202
+ <div class="flex space-x-2">
203
+ <button onclick="previousPage()" id="prevPageBtn" class="px-3 py-1 bg-gray-300 rounded hover:bg-gray-400 disabled:opacity-50" disabled>Previous</button>
204
+ <span id="pageInfo" class="px-3 py-1">-</span>
205
+ <button onclick="nextPage()" id="nextPageBtn" class="px-3 py-1 bg-gray-300 rounded hover:bg-gray-400 disabled:opacity-50" disabled>Next</button>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
210
+
211
+ <!-- Database Tab -->
212
+ <div id="databaseTab" class="tab-content hidden">
213
+ <h2 class="text-2xl font-bold mb-6">🗄️ Database Overview</h2>
214
+
215
+ <div class="admin-card">
216
+ <h3 class="text-lg font-semibold mb-4">📋 Database Schema</h3>
217
+ <div id="databaseSchema" class="space-y-4">
218
+ <div class="text-center text-gray-500 py-4">Loading schema...</div>
219
+ </div>
220
+ </div>
221
+ </div>
222
+
223
+ <!-- Token Usage Tab -->
224
+ <div id="tokensTab" class="tab-content hidden">
225
+ <div class="flex justify-between items-center mb-6">
226
+ <h2 class="text-2xl font-bold">🎯 Token Usage Analytics</h2>
227
+ <button onclick="exportTokens()" class="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600">
228
+ 📥 Export Token Data
229
+ </button>
230
+ </div>
231
+
232
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
233
+ <div class="admin-card">
234
+ <h3 class="text-lg font-semibold mb-4">📊 Usage by Provider</h3>
235
+ <canvas id="providerUsageChart" width="400" height="300"></canvas>
236
+ </div>
237
+ <div class="admin-card">
238
+ <h3 class="text-lg font-semibold mb-4">💰 Cost Breakdown</h3>
239
+ <canvas id="costBreakdownChart" width="400" height="300"></canvas>
240
+ </div>
241
+ </div>
242
+ </div>
243
+
244
+ <!-- System Health Tab -->
245
+ <div id="systemTab" class="tab-content hidden">
246
+ <h2 class="text-2xl font-bold mb-6">🖥️ System Health Monitor</h2>
247
+
248
+ <!-- System Alerts -->
249
+ <div class="admin-card mb-6">
250
+ <h3 class="text-lg font-semibold mb-4">⚠️ System Alerts</h3>
251
+ <div id="systemAlerts">
252
+ <div class="text-center text-gray-500 py-4">Loading alerts...</div>
253
+ </div>
254
+ </div>
255
+
256
+ <!-- System Metrics -->
257
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
258
+ <div class="admin-card">
259
+ <h4 class="font-semibold mb-3">💾 Memory Usage</h4>
260
+ <div id="memoryMetrics">
261
+ <div class="text-sm text-gray-500">Loading...</div>
262
+ </div>
263
+ </div>
264
+
265
+ <div class="admin-card">
266
+ <h4 class="font-semibold mb-3">💿 Disk Usage</h4>
267
+ <div id="diskMetrics">
268
+ <div class="text-sm text-gray-500">Loading...</div>
269
+ </div>
270
+ </div>
271
+
272
+ <div class="admin-card">
273
+ <h4 class="font-semibold mb-3">🗄️ Database</h4>
274
+ <div id="databaseMetrics">
275
+ <div class="text-sm text-gray-500">Loading...</div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+
280
+ <!-- Error Logs -->
281
+ <div class="admin-card">
282
+ <h3 class="text-lg font-semibold mb-4">🚨 Recent Errors</h3>
283
+ <div id="errorLogs">
284
+ <div class="text-center text-gray-500 py-4">Loading error logs...</div>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </div>
289
+ </div>
290
+
291
+ <!-- User Detail Modal -->
292
+ <div id="userDetailModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
293
+ <div class="bg-white rounded-lg p-6 max-w-4xl w-full mx-4 max-h-screen overflow-y-auto">
294
+ <div class="flex justify-between items-center mb-4">
295
+ <h3 class="text-xl font-bold">👤 User Details</h3>
296
+ <button onclick="closeUserModal()" class="text-gray-500 hover:text-gray-700">✕</button>
297
+ </div>
298
+ <div id="userDetailContent">
299
+ <div class="text-center py-8">Loading user details...</div>
300
+ </div>
301
+ </div>
302
+ </div>
303
+
304
+ <!-- Toast Notifications -->
305
+ <div id="toastContainer" class="fixed top-4 right-4 z-50"></div>
306
+
307
+ <script>
308
+ let currentUser = null;
309
+ let currentPage = 1;
310
+ let usersData = null;
311
+
312
+ // Check admin authentication on load
313
+ document.addEventListener('DOMContentLoaded', () => {
314
+ checkAdminAuth();
315
+ });
316
+
317
+ async function checkAdminAuth() {
318
+ try {
319
+ const response = await fetch('/admin/check');
320
+ const data = await response.json();
321
+
322
+ if (data.authenticated) {
323
+ showAdminInterface(data.username);
324
+ } else {
325
+ showLoginModal();
326
+ }
327
+ } catch (error) {
328
+ console.error('Auth check error:', error);
329
+ showLoginModal();
330
+ }
331
+ }
332
+
333
+ function showLoginModal() {
334
+ document.getElementById('loginModal').classList.remove('hidden');
335
+ document.getElementById('adminInterface').classList.add('hidden');
336
+ }
337
+
338
+ function showAdminInterface(username) {
339
+ document.getElementById('loginModal').classList.add('hidden');
340
+ document.getElementById('adminInterface').classList.remove('hidden');
341
+ document.getElementById('adminUserInfo').textContent = `Welcome, ${username}`;
342
+
343
+ // Load dashboard data
344
+ loadDashboard();
345
+ }
346
+
347
+ // Admin login
348
+ document.getElementById('adminLoginForm').addEventListener('submit', async (e) => {
349
+ e.preventDefault();
350
+
351
+ const username = document.getElementById('adminUsername').value;
352
+ const password = document.getElementById('adminPassword').value;
353
+ const errorDiv = document.getElementById('loginError');
354
+
355
+ try {
356
+ const response = await fetch('/admin/login', {
357
+ method: 'POST',
358
+ headers: {
359
+ 'Content-Type': 'application/json',
360
+ },
361
+ body: JSON.stringify({ username, password })
362
+ });
363
+
364
+ const data = await response.json();
365
+
366
+ if (data.success) {
367
+ showAdminInterface(username);
368
+ errorDiv.classList.add('hidden');
369
+ } else {
370
+ errorDiv.textContent = data.error || 'Login failed';
371
+ errorDiv.classList.remove('hidden');
372
+ }
373
+ } catch (error) {
374
+ errorDiv.textContent = 'Network error. Please try again.';
375
+ errorDiv.classList.remove('hidden');
376
+ }
377
+ });
378
+
379
+ async function adminLogout() {
380
+ try {
381
+ await fetch('/admin/logout', { method: 'POST' });
382
+ showLoginModal();
383
+ showToast('Logged out successfully', 'success');
384
+ } catch (error) {
385
+ console.error('Logout error:', error);
386
+ }
387
+ }
388
+
389
+ // Tab management
390
+ function showTab(tabName) {
391
+ // Hide all tabs
392
+ document.querySelectorAll('.tab-content').forEach(tab => {
393
+ tab.classList.add('hidden');
394
+ });
395
+
396
+ // Remove active class from all tab buttons
397
+ document.querySelectorAll('.admin-tab').forEach(btn => {
398
+ btn.classList.remove('active', 'border-blue-500', 'text-blue-600');
399
+ btn.classList.add('border-transparent', 'text-gray-500');
400
+ });
401
+
402
+ // Show selected tab
403
+ document.getElementById(tabName + 'Tab').classList.remove('hidden');
404
+
405
+ // Add active class to selected tab button
406
+ const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
407
+ activeBtn.classList.add('active', 'border-blue-500', 'text-blue-600');
408
+ activeBtn.classList.remove('border-transparent', 'text-gray-500');
409
+
410
+ // Load tab-specific data
411
+ if (tabName === 'dashboard') {
412
+ loadDashboard();
413
+ } else if (tabName === 'users') {
414
+ loadUsers();
415
+ } else if (tabName === 'database') {
416
+ loadDatabaseSchema();
417
+ } else if (tabName === 'tokens') {
418
+ loadTokenUsage();
419
+ } else if (tabName === 'system') {
420
+ loadSystemHealth();
421
+ }
422
+ }
423
+
424
+ async function loadDashboard() {
425
+ try {
426
+ const response = await fetch('/admin/dashboard');
427
+ const data = await response.json();
428
+
429
+ if (data.success) {
430
+ const stats = data.stats;
431
+
432
+ // Update stat cards
433
+ document.getElementById('totalUsers').textContent = stats.users?.total || 0;
434
+ document.getElementById('newUsersWeek').textContent = stats.users?.new_week || 0;
435
+ document.getElementById('totalSessions').textContent = stats.activity?.total_sessions || 0;
436
+ document.getElementById('totalTokens').textContent = (stats.api_usage?.total_tokens || 0).toLocaleString();
437
+ document.getElementById('estimatedCost').textContent = stats.api_usage?.estimated_cost || '0.00';
438
+ document.getElementById('totalContent').textContent =
439
+ (stats.activity?.total_flashcards || 0) + (stats.activity?.total_articles || 0);
440
+
441
+ // Update recent activity
442
+ updateRecentActivity(stats.recent_activity || []);
443
+
444
+ // Create charts
445
+ createCharts(stats);
446
+ }
447
+ } catch (error) {
448
+ console.error('Dashboard loading error:', error);
449
+ showToast('Failed to load dashboard', 'error');
450
+ }
451
+ }
452
+
453
+ function updateRecentActivity(activities) {
454
+ const container = document.getElementById('recentActivity');
455
+
456
+ if (activities.length === 0) {
457
+ container.innerHTML = '<div class="text-center text-gray-500 py-4">No recent activity</div>';
458
+ return;
459
+ }
460
+
461
+ container.innerHTML = activities.map(activity => `
462
+ <div class="flex items-center justify-between py-2 border-b border-gray-100">
463
+ <div>
464
+ <span class="font-medium">${activity.user}</span>
465
+ <span class="text-gray-600">performed ${activity.activity}</span>
466
+ </div>
467
+ <span class="text-sm text-gray-500">${formatDate(activity.timestamp)}</span>
468
+ </div>
469
+ `).join('');
470
+ }
471
+
472
+ async function loadUsers() {
473
+ try {
474
+ const response = await fetch(`/admin/users?page=${currentPage}&per_page=20`);
475
+ const data = await response.json();
476
+
477
+ if (data.success) {
478
+ usersData = data.data;
479
+ updateUsersTable(usersData.users);
480
+ updateUsersPagination(usersData);
481
+ }
482
+ } catch (error) {
483
+ console.error('Users loading error:', error);
484
+ showToast('Failed to load users', 'error');
485
+ }
486
+ }
487
+
488
+ function updateUsersTable(users) {
489
+ const tbody = document.getElementById('usersTableBody');
490
+
491
+ if (users.length === 0) {
492
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center py-8">No users found</td></tr>';
493
+ return;
494
+ }
495
+
496
+ tbody.innerHTML = users.map(user => `
497
+ <tr class="border-b hover:bg-gray-50">
498
+ <td class="px-4 py-3">
499
+ <div>
500
+ <div class="font-medium">${user.email}</div>
501
+ <div class="text-sm text-gray-500">ID: ${user.id}</div>
502
+ </div>
503
+ </td>
504
+ <td class="px-4 py-3 text-sm text-gray-600">
505
+ ${formatDate(user.created_at)}
506
+ </td>
507
+ <td class="px-4 py-3">
508
+ <div class="text-sm">
509
+ <div>${user.session_count} sessions</div>
510
+ <div class="text-gray-500">${user.flashcard_count} cards, ${user.article_count} articles</div>
511
+ </div>
512
+ </td>
513
+ <td class="px-4 py-3">
514
+ <span class="px-2 py-1 text-xs rounded-full ${user.email_confirmed ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}">
515
+ ${user.email_confirmed ? 'Confirmed' : 'Unconfirmed'}
516
+ </span>
517
+ </td>
518
+ <td class="px-4 py-3">
519
+ <div class="flex space-x-2">
520
+ <button onclick="viewUser(${user.id})" class="text-blue-600 hover:text-blue-800 text-sm">👁️ View</button>
521
+ <button onclick="deleteUser(${user.id}, '${user.email}')" class="text-red-600 hover:text-red-800 text-sm">🗑️ Delete</button>
522
+ </div>
523
+ </td>
524
+ </tr>
525
+ `).join('');
526
+ }
527
+
528
+ function updateUsersPagination(data) {
529
+ document.getElementById('usersInfo').textContent =
530
+ `Showing ${((data.page - 1) * data.per_page) + 1}-${Math.min(data.page * data.per_page, data.total)} of ${data.total} users`;
531
+
532
+ document.getElementById('pageInfo').textContent = `Page ${data.page} of ${data.total_pages}`;
533
+
534
+ document.getElementById('prevPageBtn').disabled = data.page <= 1;
535
+ document.getElementById('nextPageBtn').disabled = data.page >= data.total_pages;
536
+ }
537
+
538
+ async function viewUser(userId) {
539
+ try {
540
+ const response = await fetch(`/admin/users/${userId}`);
541
+ const data = await response.json();
542
+
543
+ if (data.success) {
544
+ showUserModal(data.user);
545
+ }
546
+ } catch (error) {
547
+ console.error('User details error:', error);
548
+ showToast('Failed to load user details', 'error');
549
+ }
550
+ }
551
+
552
+ function showUserModal(user) {
553
+ const modal = document.getElementById('userDetailModal');
554
+ const content = document.getElementById('userDetailContent');
555
+
556
+ content.innerHTML = `
557
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
558
+ <div>
559
+ <h4 class="font-semibold mb-3">📋 Basic Information</h4>
560
+ <div class="space-y-2 text-sm">
561
+ <div><strong>Email:</strong> ${user.user.email}</div>
562
+ <div><strong>ID:</strong> ${user.user.id}</div>
563
+ <div><strong>Created:</strong> ${formatDate(user.user.created_at)}</div>
564
+ <div><strong>Last Login:</strong> ${user.user.last_login ? formatDate(user.user.last_login) : 'Never'}</div>
565
+ <div><strong>Email Confirmed:</strong> ${user.user.email_confirmed ? '✅ Yes' : '❌ No'}</div>
566
+ </div>
567
+ </div>
568
+
569
+ <div>
570
+ <h4 class="font-semibold mb-3">⚙️ Settings</h4>
571
+ <div class="space-y-2 text-sm">
572
+ ${Object.entries(user.settings).map(([key, value]) =>
573
+ `<div><strong>${key}:</strong> ${value}</div>`
574
+ ).join('')}
575
+ </div>
576
+ </div>
577
+ </div>
578
+
579
+ <div class="mt-6">
580
+ <h4 class="font-semibold mb-3">📊 Recent Activity</h4>
581
+ <div class="max-h-48 overflow-y-auto">
582
+ ${user.recent_sessions.map(session => `
583
+ <div class="flex justify-between py-2 border-b text-sm">
584
+ <span>${session.activity}</span>
585
+ <span class="text-gray-500">${formatDate(session.timestamp)}</span>
586
+ </div>
587
+ `).join('')}
588
+ </div>
589
+ </div>
590
+
591
+ <div class="mt-6">
592
+ <h4 class="font-semibold mb-3">🎯 Token Usage</h4>
593
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
594
+ ${user.token_usage.map(usage => `
595
+ <div class="bg-gray-50 p-3 rounded">
596
+ <div class="font-medium">${usage.provider}</div>
597
+ <div class="text-sm text-gray-600">
598
+ Input: ${usage.input_tokens.toLocaleString()}<br>
599
+ Output: ${usage.output_tokens.toLocaleString()}<br>
600
+ Calls: ${usage.calls}
601
+ </div>
602
+ </div>
603
+ `).join('')}
604
+ </div>
605
+ </div>
606
+ `;
607
+
608
+ modal.classList.remove('hidden');
609
+ }
610
+
611
+ function closeUserModal() {
612
+ document.getElementById('userDetailModal').classList.add('hidden');
613
+ }
614
+
615
+ async function deleteUser(userId, email) {
616
+ if (!confirm(`Are you sure you want to delete user "${email}" and all their data? This action cannot be undone.`)) {
617
+ return;
618
+ }
619
+
620
+ try {
621
+ const response = await fetch(`/admin/users/${userId}`, {
622
+ method: 'DELETE'
623
+ });
624
+
625
+ const data = await response.json();
626
+
627
+ if (data.success) {
628
+ showToast('User deleted successfully', 'success');
629
+ loadUsers(); // Refresh the table
630
+ } else {
631
+ showToast('Failed to delete user', 'error');
632
+ }
633
+ } catch (error) {
634
+ console.error('Delete user error:', error);
635
+ showToast('Failed to delete user', 'error');
636
+ }
637
+ }
638
+
639
+ async function loadDatabaseSchema() {
640
+ try {
641
+ const response = await fetch('/admin/database/schema');
642
+ const data = await response.json();
643
+
644
+ if (data.success) {
645
+ updateDatabaseSchema(data.schema);
646
+ }
647
+ } catch (error) {
648
+ console.error('Schema loading error:', error);
649
+ showToast('Failed to load database schema', 'error');
650
+ }
651
+ }
652
+
653
+ function updateDatabaseSchema(schema) {
654
+ const container = document.getElementById('databaseSchema');
655
+
656
+ container.innerHTML = Object.entries(schema).map(([tableName, tableInfo]) => `
657
+ <div class="border rounded-lg p-4">
658
+ <div class="flex justify-between items-center mb-3">
659
+ <h5 class="font-semibold text-lg">${tableName}</h5>
660
+ <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">
661
+ ${tableInfo.row_count} rows
662
+ </span>
663
+ </div>
664
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 text-sm">
665
+ ${tableInfo.columns.map(col => `
666
+ <div class="flex items-center space-x-2">
667
+ <span class="font-medium">${col.name}</span>
668
+ <span class="text-gray-500">${col.type}</span>
669
+ ${col.primary_key ? '<span class="bg-yellow-100 text-yellow-800 px-1 rounded text-xs">PK</span>' : ''}
670
+ ${col.not_null ? '<span class="bg-red-100 text-red-800 px-1 rounded text-xs">NOT NULL</span>' : ''}
671
+ </div>
672
+ `).join('')}
673
+ </div>
674
+ </div>
675
+ `).join('');
676
+ }
677
+
678
+ async function loadTokenUsage() {
679
+ try {
680
+ // For now, create sample charts with existing data
681
+ await createTokenUsageCharts();
682
+ } catch (error) {
683
+ console.error('Token usage loading error:', error);
684
+ showToast('Failed to load token usage data', 'error');
685
+ }
686
+ }
687
+
688
+ async function createTokenUsageCharts() {
689
+ // Provider Usage Chart
690
+ const providerCtx = document.getElementById('providerUsageChart').getContext('2d');
691
+
692
+ new Chart(providerCtx, {
693
+ type: 'bar',
694
+ data: {
695
+ labels: ['Groq', 'Gemini'],
696
+ datasets: [{
697
+ label: 'Input Tokens',
698
+ data: [150000, 85000],
699
+ backgroundColor: 'rgba(59, 130, 246, 0.8)',
700
+ borderColor: 'rgba(59, 130, 246, 1)',
701
+ borderWidth: 1
702
+ }, {
703
+ label: 'Output Tokens',
704
+ data: [45000, 28000],
705
+ backgroundColor: 'rgba(147, 51, 234, 0.8)',
706
+ borderColor: 'rgba(147, 51, 234, 1)',
707
+ borderWidth: 1
708
+ }]
709
+ },
710
+ options: {
711
+ responsive: true,
712
+ plugins: {
713
+ title: {
714
+ display: true,
715
+ text: 'Token Usage by Provider'
716
+ }
717
+ },
718
+ scales: {
719
+ y: {
720
+ beginAtZero: true,
721
+ title: {
722
+ display: true,
723
+ text: 'Tokens'
724
+ }
725
+ }
726
+ }
727
+ }
728
+ });
729
+
730
+ // Cost Breakdown Chart
731
+ const costCtx = document.getElementById('costBreakdownChart').getContext('2d');
732
+
733
+ new Chart(costCtx, {
734
+ type: 'pie',
735
+ data: {
736
+ labels: ['Conversation', 'Content Analysis', 'Recommendations', 'Study Planning'],
737
+ datasets: [{
738
+ data: [40, 25, 20, 15],
739
+ backgroundColor: [
740
+ 'rgba(34, 197, 94, 0.8)',
741
+ 'rgba(59, 130, 246, 0.8)',
742
+ 'rgba(147, 51, 234, 0.8)',
743
+ 'rgba(251, 146, 60, 0.8)'
744
+ ],
745
+ borderColor: [
746
+ 'rgba(34, 197, 94, 1)',
747
+ 'rgba(59, 130, 246, 1)',
748
+ 'rgba(147, 51, 234, 1)',
749
+ 'rgba(251, 146, 60, 1)'
750
+ ],
751
+ borderWidth: 2
752
+ }]
753
+ },
754
+ options: {
755
+ responsive: true,
756
+ plugins: {
757
+ title: {
758
+ display: true,
759
+ text: 'Cost Distribution by Operation Type'
760
+ },
761
+ legend: {
762
+ position: 'bottom'
763
+ }
764
+ }
765
+ }
766
+ });
767
+ }
768
+
769
+ function refreshUsers() {
770
+ currentPage = 1;
771
+ loadUsers();
772
+ }
773
+
774
+ function previousPage() {
775
+ if (currentPage > 1) {
776
+ currentPage--;
777
+ loadUsers();
778
+ }
779
+ }
780
+
781
+ function nextPage() {
782
+ if (usersData && currentPage < usersData.total_pages) {
783
+ currentPage++;
784
+ loadUsers();
785
+ }
786
+ }
787
+
788
+ function createCharts(stats) {
789
+ createUserGrowthChart(stats);
790
+ createTokenCostChart(stats);
791
+ }
792
+
793
+ function createUserGrowthChart(stats) {
794
+ const ctx = document.getElementById('userGrowthChart').getContext('2d');
795
+
796
+ // Generate sample data for last 30 days
797
+ const dates = [];
798
+ const userData = [];
799
+ const today = new Date();
800
+
801
+ for (let i = 29; i >= 0; i--) {
802
+ const date = new Date(today);
803
+ date.setDate(date.getDate() - i);
804
+ dates.push(date.toLocaleDateString());
805
+
806
+ // Simulate growth data (would come from real analytics)
807
+ userData.push(Math.max(0, stats.users?.total - Math.floor(Math.random() * i * 2)));
808
+ }
809
+
810
+ new Chart(ctx, {
811
+ type: 'line',
812
+ data: {
813
+ labels: dates,
814
+ datasets: [{
815
+ label: 'Total Users',
816
+ data: userData,
817
+ borderColor: 'rgb(59, 130, 246)',
818
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
819
+ fill: true,
820
+ tension: 0.4
821
+ }]
822
+ },
823
+ options: {
824
+ responsive: true,
825
+ plugins: {
826
+ title: {
827
+ display: true,
828
+ text: 'User Growth Over Time'
829
+ }
830
+ },
831
+ interaction: {
832
+ intersect: false,
833
+ },
834
+ scales: {
835
+ x: {
836
+ display: true,
837
+ title: {
838
+ display: true,
839
+ text: 'Date'
840
+ }
841
+ },
842
+ y: {
843
+ display: true,
844
+ title: {
845
+ display: true,
846
+ text: 'Users'
847
+ }
848
+ }
849
+ }
850
+ }
851
+ });
852
+ }
853
+
854
+ function createTokenCostChart(stats) {
855
+ const ctx = document.getElementById('tokenCostChart').getContext('2d');
856
+
857
+ // Sample cost data
858
+ const providers = ['Groq', 'Gemini'];
859
+ const costs = [
860
+ (stats.api_usage?.estimated_cost || 0) * 0.6, // Groq portion
861
+ (stats.api_usage?.estimated_cost || 0) * 0.4 // Gemini portion
862
+ ];
863
+
864
+ new Chart(ctx, {
865
+ type: 'doughnut',
866
+ data: {
867
+ labels: providers,
868
+ datasets: [{
869
+ label: 'Cost ($)',
870
+ data: costs,
871
+ backgroundColor: [
872
+ 'rgba(59, 130, 246, 0.8)',
873
+ 'rgba(147, 51, 234, 0.8)'
874
+ ],
875
+ borderColor: [
876
+ 'rgba(59, 130, 246, 1)',
877
+ 'rgba(147, 51, 234, 1)'
878
+ ],
879
+ borderWidth: 2
880
+ }]
881
+ },
882
+ options: {
883
+ responsive: true,
884
+ plugins: {
885
+ title: {
886
+ display: true,
887
+ text: 'API Costs by Provider'
888
+ },
889
+ legend: {
890
+ position: 'bottom',
891
+ }
892
+ }
893
+ }
894
+ });
895
+ }
896
+
897
+ function exportUsers() {
898
+ window.open('/admin/export/users', '_blank');
899
+ showToast('Users data export started', 'success');
900
+ }
901
+
902
+ function exportTokens() {
903
+ window.open('/admin/export/tokens', '_blank');
904
+ showToast('Token usage data export started', 'success');
905
+ }
906
+
907
+ async function loadSystemHealth() {
908
+ try {
909
+ // Load system health metrics
910
+ const healthResponse = await fetch('/admin/system/health');
911
+ const healthData = await healthResponse.json();
912
+
913
+ if (healthData.success) {
914
+ updateSystemMetrics(healthData.health);
915
+ }
916
+
917
+ // Load system alerts
918
+ const alertsResponse = await fetch('/admin/system/alerts');
919
+ const alertsData = await alertsResponse.json();
920
+
921
+ if (alertsData.success) {
922
+ updateSystemAlerts(alertsData.alerts);
923
+ }
924
+ } catch (error) {
925
+ console.error('System health loading error:', error);
926
+ showToast('Failed to load system health data', 'error');
927
+ }
928
+ }
929
+
930
+ function updateSystemMetrics(health) {
931
+ // Memory metrics
932
+ if (health.memory) {
933
+ const memoryPercent = health.memory.percent || 0;
934
+ const memoryUsed = Math.round(health.memory.used / 1024 / 1024 / 1024 * 100) / 100;
935
+ const memoryTotal = Math.round(health.memory.total / 1024 / 1024 / 1024 * 100) / 100;
936
+
937
+ document.getElementById('memoryMetrics').innerHTML = `
938
+ <div class="space-y-2">
939
+ <div class="flex justify-between">
940
+ <span>Used:</span>
941
+ <span>${memoryUsed}GB / ${memoryTotal}GB</span>
942
+ </div>
943
+ <div class="w-full bg-gray-200 rounded-full h-2">
944
+ <div class="bg-blue-600 h-2 rounded-full" style="width: ${memoryPercent}%"></div>
945
+ </div>
946
+ <div class="text-xs text-gray-500">${memoryPercent.toFixed(1)}% used</div>
947
+ </div>
948
+ `;
949
+ }
950
+
951
+ // Disk metrics
952
+ if (health.disk) {
953
+ const diskPercent = health.disk.percent || 0;
954
+ const diskUsed = Math.round(health.disk.used / 1024 / 1024 / 1024 * 100) / 100;
955
+ const diskTotal = Math.round(health.disk.total / 1024 / 1024 / 1024 * 100) / 100;
956
+
957
+ document.getElementById('diskMetrics').innerHTML = `
958
+ <div class="space-y-2">
959
+ <div class="flex justify-between">
960
+ <span>Used:</span>
961
+ <span>${diskUsed}GB / ${diskTotal}GB</span>
962
+ </div>
963
+ <div class="w-full bg-gray-200 rounded-full h-2">
964
+ <div class="bg-green-600 h-2 rounded-full" style="width: ${diskPercent}%"></div>
965
+ </div>
966
+ <div class="text-xs text-gray-500">${diskPercent.toFixed(1)}% used</div>
967
+ </div>
968
+ `;
969
+ }
970
+
971
+ // Database metrics
972
+ if (health.database) {
973
+ document.getElementById('databaseMetrics').innerHTML = `
974
+ <div class="space-y-2">
975
+ <div class="flex justify-between">
976
+ <span>Size:</span>
977
+ <span>${health.database.size_mb}MB</span>
978
+ </div>
979
+ <div class="flex justify-between">
980
+ <span>Uptime:</span>
981
+ <span>${health.uptime || 'Unknown'}</span>
982
+ </div>
983
+ </div>
984
+ `;
985
+ }
986
+
987
+ // Error logs
988
+ if (health.recent_errors && health.recent_errors.length > 0) {
989
+ document.getElementById('errorLogs').innerHTML = `
990
+ <div class="space-y-2 max-h-64 overflow-y-auto">
991
+ ${health.recent_errors.map(error => `
992
+ <div class="p-2 bg-red-50 border border-red-200 rounded text-sm">
993
+ <div class="flex justify-between items-start">
994
+ <span class="font-medium text-red-800">${error.level}</span>
995
+ <span class="text-red-600 text-xs">${error.timestamp}</span>
996
+ </div>
997
+ <div class="text-red-700 mt-1">${error.message}</div>
998
+ <div class="text-red-600 text-xs mt-1">Module: ${error.module}</div>
999
+ </div>
1000
+ `).join('')}
1001
+ </div>
1002
+ `;
1003
+ } else {
1004
+ document.getElementById('errorLogs').innerHTML = '<div class="text-center text-gray-500 py-4">No recent errors</div>';
1005
+ }
1006
+ }
1007
+
1008
+ function updateSystemAlerts(alerts) {
1009
+ const container = document.getElementById('systemAlerts');
1010
+
1011
+ if (alerts.length === 0) {
1012
+ container.innerHTML = '<div class="text-center text-green-600 py-4">✅ All systems normal</div>';
1013
+ return;
1014
+ }
1015
+
1016
+ container.innerHTML = alerts.map(alert => {
1017
+ const bgColor = alert.type === 'error' ? 'bg-red-50 border-red-200' :
1018
+ alert.type === 'warning' ? 'bg-yellow-50 border-yellow-200' :
1019
+ 'bg-blue-50 border-blue-200';
1020
+ const textColor = alert.type === 'error' ? 'text-red-800' :
1021
+ alert.type === 'warning' ? 'text-yellow-800' :
1022
+ 'text-blue-800';
1023
+ const icon = alert.type === 'error' ? '🚨' :
1024
+ alert.type === 'warning' ? '⚠️' : 'ℹ️';
1025
+
1026
+ return `
1027
+ <div class="p-3 ${bgColor} border rounded-lg">
1028
+ <div class="flex items-start gap-3">
1029
+ <span class="text-lg">${icon}</span>
1030
+ <div class="flex-1">
1031
+ <div class="font-medium ${textColor}">${alert.message}</div>
1032
+ <div class="text-sm ${textColor} opacity-75 mt-1">Action: ${alert.action}</div>
1033
+ </div>
1034
+ </div>
1035
+ </div>
1036
+ `;
1037
+ }).join('');
1038
+ }
1039
+
1040
+ function formatDate(dateString) {
1041
+ if (!dateString) return 'Never';
1042
+ return new Date(dateString).toLocaleDateString() + ' ' + new Date(dateString).toLocaleTimeString();
1043
+ }
1044
+
1045
+ function showToast(message, type = 'info') {
1046
+ const toast = document.createElement('div');
1047
+ toast.className = `fixed top-4 right-4 p-4 rounded-lg shadow-md z-50 ${
1048
+ type === 'success' ? 'bg-green-500' :
1049
+ type === 'error' ? 'bg-red-500' :
1050
+ type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'
1051
+ } text-white`;
1052
+
1053
+ toast.textContent = message;
1054
+ document.body.appendChild(toast);
1055
+
1056
+ setTimeout(() => {
1057
+ toast.remove();
1058
+ }, 5000);
1059
+ }
1060
+
1061
+ // Add CSS for tab styling
1062
+ const style = document.createElement('style');
1063
+ style.textContent = `
1064
+ .admin-tab {
1065
+ padding: 0.5rem 1rem;
1066
+ border-bottom: 2px solid transparent;
1067
+ font-medium: 500;
1068
+ text-decoration: none;
1069
+ transition: all 0.2s;
1070
+ }
1071
+ .admin-tab:hover {
1072
+ text-decoration: none;
1073
+ border-bottom-color: #d1d5db;
1074
+ }
1075
+ .admin-tab.active {
1076
+ border-bottom-color: #3b82f6;
1077
+ color: #3b82f6;
1078
+ }
1079
+ `;
1080
+ document.head.appendChild(style);
1081
+ </script>
1082
+ </body>
1083
+ </html>
templates/index.html CHANGED
@@ -1 +1,1959 @@
1
- teste
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+