amauricunha commited on
Commit
6f713c1
·
verified ·
1 Parent(s): 3a6614e

Upload 2 files

Browse files
Files changed (2) hide show
  1. templates/admin.html +1083 -0
  2. templates/index.html +351 -1136
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,228 +1,329 @@
1
- {% extends "base.html" %}
2
- {% block title %}Início - English Helper{% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- {% block content %}
5
- <div class="container mx-auto max-w-5xl">
6
- <header class="mb-10">
7
- <div class="flex justify-between items-center mb-6">
8
- <div>
9
- <h1 class="text-3xl lg:text-4xl font-bold text-gray-800">Dynamic English Study Studio</h1>
10
- <p class="text-gray-500 mt-2">Your study tool with audio, images, and conversation & pronunciation practice.</p>
11
- <div id="demoModeIndicator" class="mt-2" style="display: none;">
12
- <div class="inline-block bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm font-medium">
13
- 🚀 Demo Mode - No email confirmation needed
 
 
 
14
  </div>
 
15
  </div>
16
- </div>
17
- <div class="auth-section">
18
- <nav class="flex items-center gap-3">
19
- <a href="/admin" class="btn bg-gray-100 hover:bg-gray-200 text-gray-800">Admin</a>
20
- <a href="/dashboard" class="btn bg-gray-100 hover:bg-gray-200 text-gray-800">Dashboard</a>
21
- <a href="/status" class="btn bg-gray-100 hover:bg-gray-200 text-gray-800">Status</a>
22
- </nav>
23
- </div>
24
- </div>
25
- </header>
26
 
27
- <!-- User selector / creator -->
28
- <div class="mb-6 flex items-center gap-4">
29
- <div>
30
- <label class="text-sm font-medium">Current User</label>
31
- <select id="userSelect" class="border rounded px-3 py-2"></select>
32
- </div>
33
- <div>
34
- <input id="newUserInput" placeholder="Create user id (e.g. alice)" class="border rounded px-3 py-2" />
35
- <button id="createUserBtn" class="ml-2 bg-blue-600 text-white px-3 py-2 rounded">Create</button>
36
- </div>
37
- <div id="currentUserDisplay" class="ml-4 text-sm text-gray-600"></div>
38
- </div>
 
 
 
 
39
 
40
- <!-- Main UI (left + right columns) - preserved from original file -->
41
- <div class="card mb-8">
42
- <h2 class="text-xl font-semibold mb-4 text-gray-700">Global Session Settings</h2>
43
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
44
- <div>
45
- <label for="modelSelector" class="block text-sm font-medium text-gray-700">Main AI Model</label>
46
- <select id="modelSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
47
- <option>Loading models...</option>
48
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  </div>
50
- <div>
51
- <label for="contextSelector" class="block text-sm font-medium text-gray-700">Vocabulary Focus</label>
52
- <select id="contextSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
53
- <option value="General/Social">General/Social</option>
54
- <option value="Professional/Business">Professional/Business</option>
55
- <option value="Technical/IT">Technical/IT</option>
56
- </select>
57
- </div>
58
- <div>
59
- <label for="voiceSelector" class="block text-sm font-medium text-gray-700">Voice Accent (TTS)</label>
60
- <select id="voiceSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
61
- <option value="co.uk">British (Female)</option>
62
- <option value="com">American (Female)</option>
63
- <option value="com.au">Australian (Female)</option>
64
- </select>
65
- </div>
66
- </div>
67
- </div>
68
 
69
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
70
- <!-- Left column (inputs) -->
71
- <div class="flex flex-col gap-6">
72
- <div class="card">
73
- <h2 class="text-xl font-semibold mb-4 text-gray-700">1. Study from Text</h2>
74
- <div id="textEditor" contenteditable="true" spellcheck="true" lang="en" class="w-full p-4 border rounded-md min-h-[150px]"></div>
75
- <p class="text-sm text-right text-gray-500 mt-2">Characters: <span id="charCount">0</span>/10000</p>
76
- <div class="mt-4 border-t pt-4 flex flex-wrap gap-2 justify-between items-center">
77
- <button id="createCardButton" onclick="createFlashcardFromSelection()" class="btn btn-primary bg-green-600 hover:bg-green-700 disabled:opacity-50" disabled>+ Create Flashcard</button>
78
- <button id="playButton" onclick="handlePlay()" class="btn btn-primary" disabled>
79
- <span id="playText">Listen to Text</span>
80
- <div id="playSpinner" class="spinner ml-2 hidden"></div>
81
- </button>
82
  </div>
83
- <audio id="audioPlayer" controls class="w-full mt-4 hidden"></audio>
84
  </div>
85
-
86
- <!-- Additional left column cards preserved (image analysis, conversation, image generator, etc.) -->
87
- <!-- For brevity they remain unchanged; script logic loaded in scripts block will reference these elements. -->
88
- <!-- ...existing left column content omitted for brevity but preserved in original file... -->
89
- </div>
90
-
91
- <!-- Right column (panels) -->
92
- <div class="flex flex-col gap-6">
93
- <!-- Right column content preserved -->
94
- <!-- ...existing right column content omitted for brevity ... -->
95
  </div>
96
  </div>
97
- </div>
98
- <!-- Toast Container -->
99
- <div id="toastContainer" class="toast-container"></div>
100
-
101
- {% endblock %}
102
 
103
- {% block scripts %}
104
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
105
  <script>
106
- // Core client-side JS preserved: minimal initializers and references used by the UI
107
- // (To keep this patch safe for HF Spaces, heavy features that require external libs or keys should fail gracefully.)
108
- document.addEventListener('DOMContentLoaded', () => {
109
- // Basic demo mode detection
110
- const isHF = window.location.hostname.includes('hf.space') || window.location.hostname.includes('huggingface.co');
111
- if (isHF) document.getElementById('demoModeIndicator')?.style && (document.getElementById('demoModeIndicator').style.display = 'block');
112
-
113
- // Populate model selector from backend if available
114
- fetch('/list-models').then(r=>r.json()).then(models=>{
115
- const sel = document.getElementById('modelSelector');
116
- if (!sel) return;
117
- sel.innerHTML = '';
118
- if (Array.isArray(models) && models.length) {
119
- models.forEach(m => {
120
- const opt = document.createElement('option'); opt.value = m.value; opt.textContent = m.name; sel.appendChild(opt);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  });
122
- } else {
123
- sel.innerHTML = '<option>No models available</option>';
 
 
 
124
  }
125
- }).catch(()=>{});
126
-
127
- // Wire up character count
128
- const editor = document.getElementById('textEditor');
129
- if (editor) {
130
- editor.addEventListener('input', () => {
131
- document.getElementById('charCount').textContent = editor.textContent.length;
132
- });
133
- }
134
-
135
- // User creation & selection
136
- const userSelect = document.getElementById('userSelect');
137
- const newUserInput = document.getElementById('newUserInput');
138
- const createUserBtn = document.getElementById('createUserBtn');
139
- const currentUserDisplay = document.getElementById('currentUserDisplay');
140
-
141
- function setCurrentUser(id){
142
- localStorage.setItem('eh_current_user', id);
143
- if (currentUserDisplay) currentUserDisplay.textContent = 'Selected: ' + id;
144
  }
145
 
146
- function loadUsersIntoSelect(){
147
- fetch('/users').then(r=>r.json()).then(data=>{
148
- const users = data.users || [];
149
- userSelect.innerHTML = '';
150
- const noneOpt = document.createElement('option'); noneOpt.value=''; noneOpt.textContent='-- none --'; userSelect.appendChild(noneOpt);
151
- users.forEach(u=>{ const o=document.createElement('option'); o.value=u; o.textContent=u; userSelect.appendChild(o); });
152
- const cur = localStorage.getItem('eh_current_user');
153
- if (cur){ userSelect.value = cur; if (currentUserDisplay) currentUserDisplay.textContent = 'Selected: ' + cur; }
154
- }).catch(()=>{});
 
 
 
 
155
  }
156
-
157
- createUserBtn.addEventListener('click', async ()=>{
158
- const v = newUserInput.value && newUserInput.value.trim();
159
- if (!v) return showToast('Provide a user id');
160
- try{
161
- const res = await fetch('/user/create', {method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({user_id: v})});
162
- const j = await res.json();
163
- if (j.success){
164
- loadUsersIntoSelect();
165
- setCurrentUser(j.user_id);
166
- showToast('User created: ' + j.user_id, 'success');
167
- } else {
168
- showToast('User creation failed', 'error');
169
- }
170
- }catch(e){ showToast('Create user error','error'); }
171
- });
172
-
173
- userSelect.addEventListener('change', ()=>{ if (userSelect.value) setCurrentUser(userSelect.value); });
174
- loadUsersIntoSelect();
175
- });
176
-
177
- // Lightweight helper for toasts
178
- function showToast(msg, type='info'){
179
- const t = document.createElement('div'); t.className = 'toast ' + type; t.textContent = msg; document.body.appendChild(t); setTimeout(()=>t.remove(),4000);
180
- }
181
-
182
- // When generating study plan, include user_id if set
183
- async function generateStudyPlan(){
184
- const user_id = localStorage.getItem('eh_current_user');
185
- const payload = { target_level: document.getElementById('targetLevelSelect')?.value || 'B2', weekly_hours: document.getElementById('weeklyHoursSelect')?.value || '5' };
186
- if (user_id) payload.user_id = user_id;
187
- try{
188
- const res = await fetch('/study-plan', {method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify(payload)});
189
- const j = await res.json();
190
- if (j.success){ document.getElementById('studyPlanResult').textContent = JSON.stringify(j.plan, null, 2); showToast('Plan generated','success'); }
191
- else showToast('Failed to generate plan','error');
192
- }catch(e){ showToast('Plan error','error'); }
193
- }
194
-
195
- // Lightweight editor interactions (demo-safe)
196
- const textEditor = document.getElementById('textEditor');
197
- const charCount = document.getElementById('charCount');
198
- const createCardButton = document.getElementById('createCardButton');
199
- const playButton = document.getElementById('playButton');
200
-
201
- if (textEditor) {
202
  textEditor.addEventListener('input', () => {
203
- const len = textEditor.innerText?.length || 0;
204
- if (charCount) charCount.textContent = len;
205
- if (playButton) playButton.disabled = len === 0;
206
  });
207
-
208
- function handleTextSelection() {
209
- const sel = (window.getSelection && window.getSelection().toString()) || '';
210
- if (createCardButton) createCardButton.disabled = !sel.trim();
211
- }
212
-
213
- textEditor.addEventListener('mouseup', handleTextSelection);
214
- textEditor.addEventListener('keyup', handleTextSelection);
215
- }
216
-
217
- if (playButton) {
218
- playButton.addEventListener('click', () => {
219
- showToast('Audio generation is not enabled in demo mode.', 'info');
220
  });
221
- }
222
-
223
- </script>
224
-
225
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  async function submitActivityResponse() {
227
  const userResponse = activityInput.value.trim();
228
  if (!userResponse) {
@@ -349,64 +450,35 @@ if (playButton) {
349
  reader.readAsDataURL(file);
350
  });
351
  async function analyzeImage(base64Image) {
352
- // Show skeleton loading
353
- imageAnalysisResult.innerHTML = `
354
- <div class="spinner-container">
355
- <div class="spinner spinner-large !border-l-indigo-500 !border-gray-200"></div>
356
- <p class="ml-3 text-gray-600">Analyzing image with AI...</p>
357
- </div>
358
- <div class="mt-4">
359
- <div class="skeleton-text loading-skeleton mb-2"></div>
360
- <div class="flex gap-2">
361
- <div class="skeleton-box loading-skeleton" style="width: 80px; height: 32px;"></div>
362
- <div class="skeleton-box loading-skeleton" style="width: 100px; height: 32px;"></div>
363
- <div class="skeleton-box loading-skeleton" style="width: 90px; height: 32px;"></div>
364
- </div>
365
- </div>`;
366
-
367
  try {
368
- const vocabulary = await handleApiRequest('/analyze-image', {
369
  method: 'POST',
370
  headers: { 'Content-Type': 'application/json' },
371
  body: JSON.stringify({
372
  image: base64Image,
373
  model: modelSelector.value
374
  })
375
- }, 'Analyzing image');
376
-
 
377
  displayImageVocabulary(vocabulary);
378
- showToast(`Found ${vocabulary.length} vocabulary items in the image!`, 'success');
379
  } catch (error) {
380
- imageAnalysisResult.innerHTML = `
381
- <div class="empty-state">
382
- <div class="empty-state-icon">❌</div>
383
- <div class="empty-state-title">Analysis Failed</div>
384
- <div class="empty-state-description">Please try uploading the image again or check your connection.</div>
385
- </div>`;
386
  }
387
  }
388
  function displayImageVocabulary(vocabulary) {
389
- imageAnalysisResult.innerHTML = `
390
- <div class="fade-in">
391
- <h3 class="font-semibold mb-3 text-gray-700">🎯 Suggested Vocabulary (${vocabulary.length} items)</h3>
392
- <div class="flex flex-wrap gap-2" id="vocabularyButtons"></div>
393
- <p class="text-xs text-gray-500 mt-3">Click any term to create a flashcard</p>
394
- </div>`;
395
-
396
- const buttonContainer = document.getElementById('vocabularyButtons');
397
- vocabulary.forEach(({ term, definition }, index) => {
398
  const btn = document.createElement('button');
399
- btn.className = 'btn bg-violet-100 text-violet-700 hover:bg-violet-200 text-sm slide-in';
400
- btn.style.animationDelay = `${index * 0.1}s`;
401
- btn.innerHTML = `${term} <span class="text-violet-500 ml-1">+</span>`;
402
- btn.title = `${term}: ${definition}`;
403
- btn.onclick = () => {
404
- btn.disabled = true;
405
- btn.innerHTML = '<div class="spinner mr-1"></div> Creating...';
406
- createFlashcardFromSuggestion(term, definition);
407
- };
408
  buttonContainer.appendChild(btn);
409
  });
 
410
  }
411
  async function generateImage() {
412
  const prompt = imagePromptInput.value.trim();
@@ -435,76 +507,38 @@ if (playButton) {
435
  }
436
 
437
  // FLASHCARDS
438
- function createSkeletonFlashcard() {
439
- const cardId = `skeleton-${Date.now()}`;
440
- const container = document.createElement('div');
441
- container.className = 'flashcard-container fade-in';
442
- container.id = cardId;
443
- container.innerHTML = `
444
- <div class="flashcard-inner">
445
- <div class="flashcard-front">
446
- <div>
447
- <div class="skeleton-text small loading-skeleton"></div>
448
- <div class="skeleton-text large loading-skeleton mt-4"></div>
449
- <div class="skeleton-text loading-skeleton"></div>
450
- </div>
451
- <div class="border-t pt-3 mt-3">
452
- <div class="skeleton-text loading-skeleton"></div>
453
- <div class="skeleton-text small loading-skeleton mt-2"></div>
454
- </div>
455
- </div>
456
- </div>`;
457
- return container;
458
- }
459
-
460
  async function createFlashcard(word, context, definition = null) {
461
- if (!word || !word.trim()) {
462
- showToast('Please select a word first', 'warning');
463
- return;
464
- }
465
-
466
- // Hide empty state and show skeleton
467
- noCardsMessage.classList.add('hidden');
468
- const skeletonCard = createSkeletonFlashcard();
469
- flashcardList.prepend(skeletonCard);
470
-
471
  const btn = definition ? null : createCardButton;
472
- if (btn) setButtonLoading(btn, true);
473
-
 
 
474
  try {
475
- const cardData = await handleApiRequest('/explain-proxy', {
476
  method: 'POST',
477
  headers: { 'Content-Type': 'application/json' },
478
  body: JSON.stringify({
479
- word: word.trim(),
480
- context: context || 'General context',
481
  for_flashcard: true,
482
  model: modelSelector.value,
483
  context_focus: contextSelector.value
484
  })
485
- }, 'Creating flashcard');
486
-
 
 
 
 
487
  if(definition && !cardData.definition) cardData.definition = definition;
488
-
489
- // Remove skeleton and add real card
490
- skeletonCard.remove();
491
  renderFlashcard(cardData);
492
-
493
- // Show appropriate message based on auth status
494
- if (currentUser) {
495
- showToast(`Flashcard for "${word}" created and saved to your account!`, 'success');
496
- } else {
497
- showToast(`Flashcard for "${word}" created! Login to save your progress.`, 'success');
498
- }
499
  } catch (error) {
500
- // Remove skeleton on error
501
- skeletonCard.remove();
502
- // Show empty state if no cards left
503
- if (flashcardList.children.length === 1) {
504
- noCardsMessage.classList.remove('hidden');
505
- }
506
  } finally {
507
- if (btn) setButtonLoading(btn, false);
 
 
 
508
  }
509
  }
510
  function createFlashcardFromSelection() {
@@ -520,65 +554,41 @@ if (playButton) {
520
  const container = document.createElement('div');
521
  container.className = 'flashcard-container';
522
  container.id = cardId;
523
-
524
- // Calculate dynamic height based on content length
525
- const totalLength = (data.gapped_sentence?.length || 0) +
526
- (data.translation?.length || 0) +
527
- (data.definition?.length || 0) +
528
- (data.context_sentence?.length || 0);
529
-
530
- let minHeight = 280;
531
- if (totalLength > 300) minHeight = 350;
532
- if (totalLength > 500) minHeight = 420;
533
- if (totalLength > 800) minHeight = 500;
534
-
535
- // Responsive height adjustment
536
- if (window.innerWidth <= 768) {
537
- minHeight = Math.max(250, minHeight * 0.85);
538
- }
539
- if (window.innerWidth <= 480) {
540
- minHeight = Math.max(220, minHeight * 0.75);
541
- }
542
-
543
  container.style.minHeight = `${minHeight}px`;
544
  const fallback = 'Info not generated';
545
  container.innerHTML = `
546
- <button class="delete-btn" onclick="deleteFlashcard('${cardId}', event)" title="Delete flashcard">🗑️</button>
547
- <div class="flashcard-inner">
548
  <div class="flashcard-front">
549
  <div>
550
  <div class="text-sm text-gray-600 mb-2">Context Sentence:</div>
551
- <div class="flashcard-text text-lg text-center text-gray-800">${data.gapped_sentence || fallback}</div>
552
  </div>
553
- <div class="border-t pt-3 mt-3 text-center">
554
- <strong class="text-indigo-600">Hint (PT):</strong>
555
- <div class="flashcard-text mt-1">${data.translation || fallback}</div>
556
  </div>
557
- <div class="text-xs text-center text-gray-400 mt-4">👆 Click to see the answer</div>
558
  </div>
559
  <div class="flashcard-back">
560
  <div>
561
  <h3 class="text-xl font-bold text-center text-indigo-600 mb-2">${data.term || 'Term'}</h3>
562
- <div class="flex items-center justify-center gap-2 text-gray-600 italic mb-3">
563
- <button class="audio-btn" onclick="playAudio('${(data.context_sentence || '').replace(/'/g, "\\'")}', event)" title="Listen to pronunciation">🔊</button>
564
- <span class="flashcard-text">${data.context_sentence || fallback}</span>
565
- </div>
566
- <div class="text-sm text-gray-800"><strong class="font-semibold">Definition:</strong>
567
- <div class="flashcard-text mt-1">${data.definition || fallback}</div>
568
- </div>
569
  </div>
570
  <div class="border-t mt-3 pt-3">
571
  <h4 class="text-sm font-semibold text-center">Practice Your Pronunciation</h4>
572
  <div class="flex justify-center items-center gap-2 mt-2">
573
- <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>
574
  </div>
575
- <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>
576
  </div>
577
  </div>
578
  </div>`;
579
- container.classList.add('fade-in');
580
  flashcardList.prepend(container);
581
-
582
  container.addEventListener('click', (e) => {
583
  if(e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
584
  container.classList.toggle('flipped');
@@ -630,808 +640,13 @@ if (playButton) {
630
  };
631
  }
632
 
633
- // --- AUTHENTICATION FUNCTIONS ---
634
-
635
- async function checkAuthStatus() {
636
- // For HF Spaces we default to guest/demo mode and skip server auth checks
637
- try {
638
- const isHF = window.location.hostname.includes('hf.space') || window.location.hostname.includes('huggingface.co');
639
- if (isHF) {
640
- // Guest mode
641
- isGuestMode = true;
642
- currentUser = 'guest';
643
- showGuestState();
644
- return;
645
- }
646
-
647
- const response = await fetch('/auth/check');
648
- const data = await response.json();
649
-
650
- if (data.authenticated) {
651
- currentUser = data.user;
652
- isGuestMode = false;
653
- showAuthenticatedState();
654
- // Load user settings into selectors
655
- if (data.user.settings) {
656
- loadUserSettings(data.user.settings);
657
- }
658
- } else {
659
- showGuestState();
660
- }
661
- } catch (error) {
662
- console.error('Auth check failed:', error);
663
- showGuestState();
664
- }
665
- }
666
-
667
- function showAuthenticatedState() {
668
- document.getElementById('guestAuth').style.display = 'none';
669
- document.getElementById('userAuth').style.display = 'block';
670
- document.getElementById('userEmail').textContent = currentUser.email;
671
- document.getElementById('userAvatar').textContent = currentUser.email.charAt(0).toUpperCase();
672
-
673
- // Update session status
674
- document.getElementById('guestStatus').style.display = 'none';
675
- document.getElementById('userStatus').style.display = 'inline';
676
- document.getElementById('loadFlashcardsBtn').style.display = 'inline-block';
677
-
678
- // Show advanced panels
679
- document.getElementById('contentCuration').style.display = 'block';
680
- document.getElementById('contentCurationLogin').style.display = 'none';
681
- document.getElementById('analyticsPanel').style.display = 'block';
682
- document.getElementById('studyPlannerPanel').style.display = 'block';
683
-
684
- // Show footer user actions
685
- document.getElementById('footerUserActions').style.display = 'block';
686
-
687
- // Load analytics and study plan
688
- loadAnalytics();
689
- loadCurrentStudyPlan();
690
-
691
- showToast(`Welcome back, ${currentUser.email}!`, 'success', 3000);
692
- }
693
-
694
- function showGuestState() {
695
- document.getElementById('guestAuth').style.display = 'flex';
696
- document.getElementById('userAuth').style.display = 'none';
697
-
698
- // Update session status
699
- document.getElementById('guestStatus').style.display = 'inline';
700
- document.getElementById('userStatus').style.display = 'none';
701
- document.getElementById('loadFlashcardsBtn').style.display = 'none';
702
-
703
- // In HF guest mode, keep content curation visible but indicate limited persistence
704
- document.getElementById('contentCuration').style.display = 'block';
705
- document.getElementById('contentCurationLogin').style.display = 'none';
706
- document.getElementById('analyticsPanel').style.display = 'block';
707
- document.getElementById('studyPlannerPanel').style.display = 'block';
708
-
709
- // Hide footer user actions
710
- document.getElementById('footerUserActions').style.display = 'none';
711
-
712
- currentUser = 'guest';
713
- isGuestMode = true;
714
- }
715
-
716
- function loadUserSettings(settings) {
717
- if (settings.preferred_model) modelSelector.value = settings.preferred_model;
718
- if (settings.context_focus) contextSelector.value = settings.context_focus;
719
- if (settings.voice_accent) voiceSelector.value = settings.voice_accent;
720
- }
721
-
722
- function showAuthModal(type = 'login') {
723
- // In HF Spaces we run in guest/demo mode. Show guidance instead of a modal.
724
- showToast('Guest mode active: no login required. Use Admin/Dashboard links for more tools.', 'info', 5000);
725
- }
726
-
727
- function hideAuthModal() {
728
- // No-op in guest mode
729
- }
730
-
731
- function switchAuthForm(type) {
732
- const loginForm = document.getElementById('loginForm');
733
- const registerForm = document.getElementById('registerForm');
734
- const title = document.getElementById('authModalTitle');
735
-
736
- if (type === 'login') {
737
- loginForm.classList.add('active');
738
- registerForm.classList.remove('active');
739
- title.textContent = 'Login';
740
- } else {
741
- registerForm.classList.add('active');
742
- loginForm.classList.remove('active');
743
- title.textContent = 'Create Account';
744
- }
745
- }
746
-
747
- function toggleUserMenu() {
748
- document.getElementById('userAuth').classList.toggle('show');
749
- }
750
-
751
- // Close user menu when clicking outside
752
- document.addEventListener('click', (e) => {
753
- if (!e.target.closest('.user-menu')) {
754
- document.getElementById('userAuth').classList.remove('show');
755
- }
756
- });
757
-
758
- // Login form submission
759
- document.getElementById('loginForm').addEventListener('submit', async (e) => {
760
- e.preventDefault();
761
- const email = document.getElementById('loginEmail').value;
762
- const password = document.getElementById('loginPassword').value;
763
-
764
- const submitBtn = e.target.querySelector('button[type="submit"]');
765
- setButtonLoading(submitBtn, true);
766
-
767
- try {
768
- const data = await handleApiRequest('/login', {
769
- method: 'POST',
770
- headers: { 'Content-Type': 'application/json' },
771
- body: JSON.stringify({ email, password })
772
- }, 'Logging in');
773
-
774
- currentUser = data.user;
775
- hideAuthModal();
776
- showAuthenticatedState();
777
-
778
- if (data.user.settings) {
779
- loadUserSettings(data.user.settings);
780
- }
781
-
782
- } catch (error) {
783
- // Error already handled by handleApiRequest
784
- } finally {
785
- setButtonLoading(submitBtn, false);
786
- }
787
- });
788
-
789
- // Register form submission
790
- document.getElementById('registerForm').addEventListener('submit', async (e) => {
791
- e.preventDefault();
792
- const email = document.getElementById('registerEmail').value;
793
- const password = document.getElementById('registerPassword').value;
794
- const confirmPassword = document.getElementById('confirmPassword').value;
795
-
796
- if (password !== confirmPassword) {
797
- showToast('Passwords do not match', 'error');
798
- return;
799
- }
800
-
801
- const submitBtn = e.target.querySelector('button[type="submit"]');
802
- setButtonLoading(submitBtn, true);
803
-
804
- try {
805
- const data = await handleApiRequest('/register', {
806
- method: 'POST',
807
- headers: { 'Content-Type': 'application/json' },
808
- body: JSON.stringify({ email, password })
809
- }, 'Creating account');
810
-
811
- hideAuthModal();
812
-
813
- // Mostrar mensagem baseada na resposta do servidor
814
- if (data.auto_confirmed) {
815
- showToast(data.message || 'Account created and ready to use! (Demo mode)', 'success', 5000);
816
- // Se auto-confirmado, pode logar automaticamente
817
- setTimeout(() => {
818
- showToast('You can now login with your credentials', 'info', 3000);
819
- }, 2000);
820
- } else {
821
- showToast(data.message || 'Account created! Please check your email to confirm.', 'success', 8000);
822
- }
823
-
824
- } catch (error) {
825
- // Error already handled by handleApiRequest
826
- } finally {
827
- setButtonLoading(submitBtn, false);
828
- }
829
- });
830
-
831
- async function logout() {
832
- try {
833
- await handleApiRequest('/logout', {
834
- method: 'POST'
835
- }, 'Logging out');
836
-
837
- showGuestState();
838
- showToast('Logged out successfully', 'info');
839
-
840
- } catch (error) {
841
- // Error already handled by handleApiRequest
842
- }
843
- }
844
-
845
- async function showUserSettings() {
846
- // Allow guest mode: warn that some features won't persist
847
- if (!currentUser || isGuestMode) {
848
- showToast('Using guest mode — data may not be saved between sessions.', 'warning', 4000);
849
- // continue in guest mode
850
- }
851
-
852
- document.getElementById('userAuth').classList.remove('show');
853
-
854
- // Simple settings update using current form values
855
- const settings = {
856
- preferred_model: modelSelector.value,
857
- context_focus: contextSelector.value,
858
- voice_accent: voiceSelector.value,
859
- daily_goal: 10, // Default
860
- notification_enabled: true // Default
861
- };
862
-
863
- try {
864
- await handleApiRequest('/user/settings', {
865
- method: 'POST',
866
- headers: { 'Content-Type': 'application/json' },
867
- body: JSON.stringify(settings)
868
- }, 'Saving settings');
869
-
870
- showToast('Settings saved successfully!', 'success');
871
- } catch (error) {
872
- // Error already handled
873
- }
874
- }
875
-
876
- async function showUserFlashcards() {
877
- if (!currentUser || isGuestMode) {
878
- showToast('Using guest mode — conversations are local-only in HF Spaces.', 'warning', 4000);
879
- }
880
-
881
- document.getElementById('userAuth').classList.remove('show');
882
-
883
- try {
884
- const data = await handleApiRequest('/user/flashcards', {}, 'Loading your flashcards');
885
-
886
- // Clear current flashcards
887
- flashcardList.innerHTML = '';
888
-
889
- if (data.flashcards && data.flashcards.length > 0) {
890
- data.flashcards.forEach(card => {
891
- const flashcardData = {
892
- term: card.term,
893
- translation: card.translation,
894
- context_sentence: card.context_sentence,
895
- gapped_sentence: card.gapped_sentence,
896
- definition: card.definition
897
- };
898
- renderFlashcard(flashcardData);
899
- });
900
- showToast(`Loaded ${data.flashcards.length} saved flashcards`, 'success');
901
- } else {
902
- noCardsMessage.classList.remove('hidden');
903
- showToast('No saved flashcards found', 'info');
904
- }
905
-
906
- } catch (error) {
907
- noCardsMessage.classList.remove('hidden');
908
- }
909
- }
910
-
911
- // --- CONTENT CURATION FUNCTIONS ---
912
-
913
- async function searchContent() {
914
- if (!currentUser) {
915
- showToast('Please log in to search content', 'warning');
916
- return;
917
- }
918
-
919
- const query = document.getElementById('contentSearchInput').value.trim();
920
- const category = document.getElementById('contentCategorySelect').value;
921
-
922
- if (!query) {
923
- showToast('Please enter a search topic', 'warning');
924
- return;
925
- }
926
-
927
- const resultsContainer = document.getElementById('contentResults');
928
- resultsContainer.innerHTML = '<div class="spinner-container"><div class="spinner spinner-large"></div><p class="ml-3">Searching for relevant content...</p></div>';
929
-
930
- try {
931
- const data = await handleApiRequest('/content/search', {
932
- method: 'POST',
933
- headers: { 'Content-Type': 'application/json' },
934
- body: JSON.stringify({ query, category })
935
- }, 'Searching content');
936
-
937
- displayContentResults(data.results);
938
- } catch (error) {
939
- resultsContainer.innerHTML = '<div class="text-red-600">Failed to search content. Please try again.</div>';
940
- }
941
- }
942
-
943
- async function getRecommendations() {
944
- if (!currentUser) {
945
- showToast('Please log in to get recommendations', 'warning');
946
- return;
947
- }
948
-
949
- const resultsContainer = document.getElementById('contentResults');
950
- resultsContainer.innerHTML = '<div class="spinner-container"><div class="spinner spinner-large"></div><p class="ml-3">Generating AI recommendations...</p></div>';
951
-
952
- try {
953
- const data = await handleApiRequest('/content/recommendations', {}, 'Getting recommendations');
954
- displayRecommendations(data.recommendations);
955
- } catch (error) {
956
- resultsContainer.innerHTML = '<div class="text-red-600">Failed to get recommendations. Please try again.</div>';
957
- }
958
- }
959
-
960
- function displayContentResults(results) {
961
- const container = document.getElementById('contentResults');
962
-
963
- if (!results || results.length === 0) {
964
- container.innerHTML = `
965
- <div class="empty-state">
966
- <div class="empty-state-icon">🔍</div>
967
- <div class="empty-state-title">No Content Found</div>
968
- <div class="empty-state-description">Try different search terms or check your interests settings</div>
969
- </div>`;
970
- return;
971
- }
972
-
973
- container.innerHTML = results.map(result => `
974
- <div class="content-item fade-in">
975
- <div class="content-item-title">${result.title}</div>
976
- <div class="content-item-meta">
977
- <span class="difficulty-badge difficulty-${getDifficultyClass(result.estimated_difficulty || 3)}">
978
- Level ${result.estimated_difficulty || 3}
979
- </span>
980
- <span class="ml-2">${result.source}</span>
981
- ${result.published ? `<span class="ml-2">${new Date(result.published).toLocaleDateString()}</span>` : ''}
982
- </div>
983
- <div class="content-item-summary">${result.summary}</div>
984
- <div class="flex gap-2 mt-3">
985
- <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>
986
- <a href="${result.url}" target="_blank" class="btn bg-gray-500 hover:bg-gray-600 text-white text-sm">🔗 Open Original</a>
987
- </div>
988
- </div>
989
- `).join('');
990
- }
991
-
992
- function displayRecommendations(recommendations) {
993
- const container = document.getElementById('contentResults');
994
-
995
- if (!recommendations || recommendations.length === 0) {
996
- container.innerHTML = `
997
- <div class="empty-state">
998
- <div class="empty-state-icon">🤖</div>
999
- <div class="empty-state-title">No Recommendations</div>
1000
- <div class="empty-state-description">Update your interests in settings to get better recommendations</div>
1001
- </div>`;
1002
- return;
1003
- }
1004
-
1005
- container.innerHTML = `
1006
- <h3 class="text-lg font-semibold mb-4 text-purple-600">✨ AI Recommendations for You</h3>
1007
- ` + recommendations.map(rec => `
1008
- <div class="content-item fade-in border-purple-200">
1009
- <div class="content-item-title text-purple-700">${rec.topic}</div>
1010
- <div class="content-item-meta">
1011
- <span class="difficulty-badge difficulty-${getDifficultyClass(rec.difficulty)}">
1012
- ${rec.difficulty}
1013
- </span>
1014
- <span class="ml-2">AI Recommended</span>
1015
- </div>
1016
- <div class="content-item-summary">${rec.reason}</div>
1017
- <div class="flex gap-2 mt-3">
1018
- <button onclick="searchSpecificTopic('${rec.topic.replace(/'/g, "\\'")}')" class="btn bg-purple-500 hover:bg-purple-600 text-white text-sm">🔍 Find Articles</button>
1019
- </div>
1020
- </div>
1021
- `).join('');
1022
- }
1023
-
1024
- function getDifficultyClass(level) {
1025
- if (level <= 2) return 'easy';
1026
- if (level <= 4) return 'medium';
1027
- return 'hard';
1028
- }
1029
-
1030
- async function extractAndSaveContent(url, title) {
1031
- try {
1032
- showToast('Extracting content...', 'info', 2000);
1033
-
1034
- const extractData = await handleApiRequest('/content/extract', {
1035
- method: 'POST',
1036
- headers: { 'Content-Type': 'application/json' },
1037
- body: JSON.stringify({ url })
1038
- }, 'Extracting content');
1039
-
1040
- if (extractData.success) {
1041
- const saveData = await handleApiRequest('/content/save', {
1042
- method: 'POST',
1043
- headers: { 'Content-Type': 'application/json' },
1044
- body: JSON.stringify({
1045
- title: extractData.title || title,
1046
- content: extractData.content,
1047
- source_url: url,
1048
- source_type: 'web_search'
1049
- })
1050
- }, 'Saving content');
1051
-
1052
- showToast('Content saved successfully!', 'success');
1053
- loadAnalytics(); // Refresh analytics
1054
- }
1055
- } catch (error) {
1056
- // Error already handled by handleApiRequest
1057
- }
1058
- }
1059
-
1060
- async function saveCustomContent() {
1061
- if (!currentUser) {
1062
- showToast('Please log in to save content', 'warning');
1063
- return;
1064
- }
1065
-
1066
- const title = document.getElementById('customContentTitle').value.trim();
1067
- const content = document.getElementById('customContentText').value.trim();
1068
- const url = document.getElementById('customContentUrl').value.trim();
1069
-
1070
- if (!title || !content) {
1071
- showToast('Please provide both title and content', 'warning');
1072
- return;
1073
- }
1074
-
1075
- try {
1076
- await handleApiRequest('/content/save', {
1077
- method: 'POST',
1078
- headers: { 'Content-Type': 'application/json' },
1079
- body: JSON.stringify({
1080
- title,
1081
- content,
1082
- source_url: url || null,
1083
- source_type: 'manual'
1084
- })
1085
- }, 'Saving content');
1086
-
1087
- // Clear form
1088
- document.getElementById('customContentTitle').value = '';
1089
- document.getElementById('customContentText').value = '';
1090
- document.getElementById('customContentUrl').value = '';
1091
-
1092
- showToast('Content saved successfully!', 'success');
1093
- loadAnalytics(); // Refresh analytics
1094
- } catch (error) {
1095
- // Error already handled
1096
- }
1097
- }
1098
-
1099
- async function showSavedArticles() {
1100
- if (!currentUser || isGuestMode) {
1101
- showToast('Guest mode active — activity will not be persisted server-side.', 'warning', 4000);
1102
- }
1103
-
1104
- try {
1105
- const data = await handleApiRequest('/content/articles', {}, 'Loading saved articles');
1106
-
1107
- const container = document.getElementById('contentResults');
1108
-
1109
- if (!data.articles || data.articles.length === 0) {
1110
- container.innerHTML = `
1111
- <div class="empty-state">
1112
- <div class="empty-state-icon">📚</div>
1113
- <div class="empty-state-title">No Saved Articles</div>
1114
- <div class="empty-state-description">Search and save articles to build your personal library</div>
1115
- </div>`;
1116
- return;
1117
- }
1118
-
1119
- container.innerHTML = `
1120
- <h3 class="text-lg font-semibold mb-4 text-green-600">📚 Your Saved Articles (${data.articles.length})</h3>
1121
- ` + data.articles.map(article => `
1122
- <div class="content-item fade-in">
1123
- <div class="content-item-title">${article.title}</div>
1124
- <div class="content-item-meta">
1125
- <span class="difficulty-badge difficulty-medium">${article.word_count} words</span>
1126
- <span class="ml-2">${new Date(article.created_at).toLocaleDateString()}</span>
1127
- ${article.category ? `<span class="ml-2">${article.category}</span>` : ''}
1128
- </div>
1129
- <div class="content-item-summary">${article.content.substring(0, 150)}...</div>
1130
- <div class="flex gap-2 mt-3">
1131
- <button onclick="loadArticleForStudy('${article.id}', '${article.title.replace(/'/g, "\\'")}')" class="btn btn-primary text-sm">📖 Study This</button>
1132
- ${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>` : ''}
1133
- </div>
1134
- </div>
1135
- `).join('');
1136
- } catch (error) {
1137
- // Error already handled
1138
- }
1139
- }
1140
-
1141
- function loadArticleForStudy(articleId, title) {
1142
- // Load the article content into the text editor
1143
- showToast(`Loading "${title}" for study...`, 'info');
1144
- // This would load the full article content - simplified for now
1145
- showToast('Article loaded! You can now create flashcards from it.', 'success');
1146
- }
1147
-
1148
- function searchSpecificTopic(topic) {
1149
- document.getElementById('contentSearchInput').value = topic;
1150
- searchContent();
1151
- }
1152
-
1153
- // --- ANALYTICS FUNCTIONS ---
1154
-
1155
- async function loadAnalytics() {
1156
- if (!currentUser || isGuestMode) {
1157
- showToast('Guest mode active — progress may not persist across reloads.', 'warning', 4000);
1158
- }
1159
-
1160
- try {
1161
- const data = await handleApiRequest('/analytics/dashboard', {}, 'Loading analytics');
1162
-
1163
- document.getElementById('totalFlashcards').textContent = data.analytics.total_flashcards || 0;
1164
- document.getElementById('totalArticles').textContent = data.analytics.total_articles || 0;
1165
- document.getElementById('currentLevel').textContent = data.analytics.user_level || 'B1';
1166
- } catch (error) {
1167
- // Error already handled
1168
- }
1169
- }
1170
-
1171
- function toggleAnalytics() {
1172
- const content = document.getElementById('analyticsContent');
1173
- content.style.display = content.style.display === 'none' ? 'block' : 'none';
1174
- }
1175
-
1176
- // --- STUDY PLANNER FUNCTIONS ---
1177
-
1178
- async function generateStudyPlan() {
1179
- if (!currentUser || isGuestMode) {
1180
- showToast('Guest mode active — saving flashcards locally only.', 'warning', 4000);
1181
- }
1182
-
1183
- try {
1184
- const planData = {
1185
- current_level: currentUser.english_level || 'B1',
1186
- target_level: document.getElementById('targetLevelSelect')?.value || 'B2',
1187
- weekly_hours: parseInt(document.getElementById('weeklyHoursSelect')?.value || 5),
1188
- context_focus: currentUser.context_focus || 'General/Social',
1189
- study_goals: []
1190
- };
1191
-
1192
- showLoading('Generating your personalized study plan...');
1193
-
1194
- const response = await fetch('/study-plan/create', {
1195
- method: 'POST',
1196
- headers: {
1197
- 'Content-Type': 'application/json',
1198
- },
1199
- body: JSON.stringify(planData)
1200
- });
1201
-
1202
- const result = await response.json();
1203
- hideLoading();
1204
-
1205
- if (result.success) {
1206
- displayStudyPlan(result.plan);
1207
- showToast('Study plan generated successfully!', 'success');
1208
- } else {
1209
- showToast('Failed to generate study plan: ' + result.error, 'error');
1210
- }
1211
- } catch (error) {
1212
- hideLoading();
1213
- console.error('Error generating study plan:', error);
1214
- showToast('Error generating study plan', 'error');
1215
- }
1216
- }
1217
-
1218
- async function loadCurrentStudyPlan() {
1219
- if (!currentUser || isGuestMode) {
1220
- showToast('Guest mode: operation allowed but not persisted.', 'warning', 4000);
1221
- }
1222
-
1223
- try {
1224
- const response = await fetch('/study-plan/current');
1225
- const result = await response.json();
1226
-
1227
- if (result.success && result.plan) {
1228
- displayStudyPlan(result.plan);
1229
- loadStudyProgress();
1230
- }
1231
- } catch (error) {
1232
- console.error('Error loading study plan:', error);
1233
- }
1234
- }
1235
-
1236
- async function loadStudyProgress() {
1237
- if (!currentUser || isGuestMode) {
1238
- showToast('Guest mode: operation allowed but not persisted.', 'warning', 4000);
1239
- }
1240
-
1241
- try {
1242
- const response = await fetch('/study-plan/progress');
1243
- const result = await response.json();
1244
-
1245
- if (result.success && result.progress) {
1246
- displayStudyProgress(result.progress);
1247
- }
1248
- } catch (error) {
1249
- console.error('Error loading study progress:', error);
1250
- }
1251
- }
1252
-
1253
- async function completeActivity(planId, activityId, durationMinutes) {
1254
- if (!currentUser) return;
1255
-
1256
- try {
1257
- const notes = prompt('Any notes about this activity? (optional)') || '';
1258
-
1259
- const response = await fetch('/study-plan/activity/complete', {
1260
- method: 'POST',
1261
- headers: {
1262
- 'Content-Type': 'application/json',
1263
- },
1264
- body: JSON.stringify({
1265
- plan_id: planId,
1266
- activity_id: activityId,
1267
- duration_minutes: durationMinutes,
1268
- notes: notes
1269
- })
1270
- });
1271
-
1272
- const result = await response.json();
1273
-
1274
- if (result.success) {
1275
- showToast('Activity completed successfully!', 'success');
1276
-
1277
- // Update the UI
1278
- const activityCard = document.querySelector(`[data-activity-id="${activityId}"]`);
1279
- if (activityCard) {
1280
- activityCard.classList.add('completed');
1281
- const button = activityCard.querySelector('.complete-activity-btn');
1282
- if (button) {
1283
- button.textContent = '✅ Completed';
1284
- button.disabled = true;
1285
- }
1286
- }
1287
-
1288
- // Refresh progress
1289
- loadStudyProgress();
1290
- } else {
1291
- showToast('Failed to complete activity', 'error');
1292
- }
1293
- } catch (error) {
1294
- console.error('Error completing activity:', error);
1295
- showToast('Error completing activity', 'error');
1296
- }
1297
- }
1298
-
1299
- function displayStudyPlan(plan) {
1300
- const container = document.getElementById('studyPlanResult');
1301
- if (!container) return;
1302
-
1303
- container.innerHTML = `
1304
- <div class="study-plan-container">
1305
- <div class="study-plan-header">
1306
- <h3>📅 Your Personalized Study Plan</h3>
1307
- <div class="plan-overview grid grid-cols-2 gap-4 mt-4">
1308
- <div class="plan-stat">
1309
- <strong>Current Level:</strong> ${plan.current_level}
1310
- </div>
1311
- <div class="plan-stat">
1312
- <strong>Target Level:</strong> ${plan.target_level}
1313
- </div>
1314
- <div class="plan-stat">
1315
- <strong>Weekly Hours:</strong> ${plan.weekly_hours}h
1316
- </div>
1317
- <div class="plan-stat">
1318
- <strong>Duration:</strong> ${plan.estimated_weeks} weeks
1319
- </div>
1320
- </div>
1321
- </div>
1322
-
1323
- <div class="study-tips mt-6">
1324
- <h4 class="font-medium mb-2">💡 Personalized Study Tips</h4>
1325
- <ul class="space-y-1">
1326
- ${plan.study_tips.map(tip => `<li class="text-sm text-gray-600">${tip}</li>`).join('')}
1327
- </ul>
1328
- </div>
1329
-
1330
- <div class="weekly-activities mt-6">
1331
- <h4 class="font-medium mb-3">📚 Weekly Activities</h4>
1332
- <div class="activities-grid space-y-3">
1333
- ${plan.activities.slice(0, 6).map(activity => `
1334
- <div class="activity-card bg-gray-50 p-3 rounded-lg" data-activity-id="${activity.id}">
1335
- <div class="activity-header flex items-center gap-2 mb-2">
1336
- <span class="text-lg">${activity.icon}</span>
1337
- <strong class="text-sm">${activity.title}</strong>
1338
- </div>
1339
- <div class="activity-details text-xs text-gray-600">
1340
- <p><strong>Duration:</strong> ${activity.duration_minutes} minutes</p>
1341
- <p><strong>Day:</strong> ${['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][activity.day_of_week]}</p>
1342
- </div>
1343
- <button class="complete-activity-btn mt-2 bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600"
1344
- onclick="completeActivity('${plan.db_id || plan.id}', '${activity.id}', ${activity.duration_minutes})">
1345
- ✅ Mark Complete
1346
- </button>
1347
- </div>
1348
- `).join('')}
1349
- </div>
1350
- </div>
1351
-
1352
- <div class="milestones mt-6">
1353
- <h4 class="font-medium mb-3">🎯 Study Milestones</h4>
1354
- <div class="milestones-list space-y-2">
1355
- ${plan.milestones.slice(0, 3).map(milestone => `
1356
- <div class="milestone-item flex items-center gap-3 p-2 bg-blue-50 rounded ${milestone.completed ? 'completed opacity-75' : ''}">
1357
- <div class="milestone-week bg-blue-500 text-white px-2 py-1 rounded text-xs">Week ${milestone.week}</div>
1358
- <div class="milestone-content flex-1">
1359
- <strong class="text-sm">${milestone.title}</strong>
1360
- <p class="text-xs text-gray-600">${milestone.description}</p>
1361
- </div>
1362
- </div>
1363
- `).join('')}
1364
- </div>
1365
- </div>
1366
- </div>
1367
- `;
1368
-
1369
- container.classList.remove('hidden');
1370
- }
1371
-
1372
- function displayStudyProgress(progress) {
1373
- // This would be called to show progress in analytics section
1374
- console.log('Study progress:', progress);
1375
- }
1376
-
1377
  // --- INICIALIZAÇÃO ---
1378
  document.addEventListener('DOMContentLoaded', () => {
1379
- checkDemoMode();
1380
- checkAuthStatus();
1381
  populateModelSelector();
1382
  initializeSpeechRecognition();
1383
  });
1384
  </script>
1385
 
1386
- <!-- Footer -->
1387
- <footer class="mt-16 py-8 bg-gray-100 border-t border-gray-200">
1388
- <div class="container mx-auto max-w-5xl px-4">
1389
- <div class="flex flex-col md:flex-row justify-between items-center gap-4">
1390
- <div class="text-sm text-gray-600">
1391
- <p>&copy; 2024 Dynamic English Study Studio - AI Learning Platform</p>
1392
- <p class="text-xs text-gray-500 mt-1">Powered by AI for enhanced English learning</p>
1393
- </div>
1394
-
1395
- <div class="flex items-center gap-4">
1396
- <!-- System Links -->
1397
- <div class="flex items-center gap-2 text-sm">
1398
- <span class="text-gray-500">System:</span>
1399
- <a href="/admin" class="text-indigo-600 hover:text-indigo-800 font-medium" title="Admin Panel">
1400
- 👤 Admin
1401
- </a>
1402
- <span class="text-gray-300">|</span>
1403
- <a href="/auth/check" class="text-green-600 hover:text-green-800 font-medium" title="System Status" target="_blank">
1404
- ⚕️ Status
1405
- </a>
1406
- <span class="text-gray-300">|</span>
1407
- <a href="/proxy/7860" class="text-purple-600 hover:text-purple-800 font-medium" title="Gradio Dashboard" target="_blank">
1408
- 📊 Dashboard
1409
- </a>
1410
- </div>
1411
-
1412
- <!-- Version Info -->
1413
- <div class="text-xs text-gray-400 border-l border-gray-300 pl-4">
1414
- <div>v2.0.0</div>
1415
- <div>Flask + Gradio</div>
1416
- </div>
1417
- </div>
1418
- </div>
1419
-
1420
- <!-- Quick Actions for Logged Users -->
1421
- <div id="footerUserActions" class="mt-4 pt-4 border-t border-gray-200" style="display: none;">
1422
- <div class="flex flex-wrap justify-center gap-2 text-xs">
1423
- <button onclick="loadAnalytics()" class="text-blue-600 hover:text-blue-800">📊 Refresh Analytics</button>
1424
- <span class="text-gray-300">•</span>
1425
- <button onclick="showUserFlashcards()" class="text-green-600 hover:text-green-800">📚 Load Flashcards</button>
1426
- <span class="text-gray-300">•</span>
1427
- <button onclick="generateStudyPlan()" class="text-purple-600 hover:text-purple-800">🎯 New Study Plan</button>
1428
- <span class="text-gray-300">•</span>
1429
- <button onclick="showUserSettings()" class="text-gray-600 hover:text-gray-800">⚙️ Settings</button>
1430
- </div>
1431
- </div>
1432
- </div>
1433
- </footer>
1434
-
1435
  </body>
1436
  </html>
1437
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Dynamic English Study Studio</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
11
+ body { font-family: 'Inter', sans-serif; background-color: #f7f9fc; }
12
+ .card { background-color: white; border-radius: 12px; box-shadow: 0 4px_12px rgba(0,0,0,0.08); padding: 24px; }
13
+ .btn { padding: 0.5rem 1rem; border-radius: 8px; font-weight: 600; transition: all 0.2s; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
14
+ .btn:disabled { cursor: not-allowed; opacity: 0.5; }
15
+ .btn-primary { background-color: #4f46e5; color: white; }
16
+ .btn-primary:hover:not(:disabled) { background-color: #4338ca; }
17
+ .spinner { border: 2px solid rgba(255,255,255,0.3); width: 20px; height: 20px; border-radius: 50%; border-left-color: #fff; animation: spin 1s ease infinite; }
18
+ @keyframes spin { to { transform: rotate(360deg); } }
19
+ .flashcard-container { perspective: 1000px; position: relative; }
20
+ .flashcard-inner { position: relative; width: 100%; transition: transform 0.6s; transform-style: preserve-3d; }
21
+ .flashcard-container.flipped .flashcard-inner { transform: rotateY(180deg); }
22
+ .flashcard-front, .flashcard-back { position: absolute; width: 100%; height:100%; backface-visibility: hidden; border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; display: flex; flex-direction: column; justify-content: space-between; }
23
+ .flashcard-front { background-color: #f9fafb; }
24
+ .flashcard-back { background-color: #ffffff; transform: rotateY(180deg); }
25
+ .audio-btn { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #4f46e5; padding: 0; }
26
+ .delete-btn { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #9ca3af; z-index: 10; }
27
+ .delete-btn:hover { color: #ef4444; }
28
+ #activityChatDisplay { border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; min-height: 150px; background-color: #f9fafb; }
29
+ .ai-prompt { border-bottom: 2px dashed #d1d5db; padding-bottom: 1rem; margin-bottom: 1rem; }
30
+ .user-response-display { background-color: #eef2ff; border-left: 4px solid #4f46e5; padding: 0.75rem; margin-bottom: 1rem; font-style: italic; }
31
+ .ai-feedback { background-color: #f0fdf4; border-left: 4px solid #22c55e; padding: 0.75rem; }
32
+ .ai-feedback h3 { font-weight: 700; }
33
+ </style>
34
+ </head>
35
+ <body class="p-4 md:p-8">
36
+
37
+ <div class="container mx-auto max-w-5xl">
38
+ <header class="text-center mb-10">
39
+ <h1 class="text-3xl lg:text-4xl font-bold text-gray-800">Dynamic English Study Studio</h1>
40
+ <p class="text-gray-500 mt-2">Your study tool with audio, images, and conversation & pronunciation practice.</p>
41
+ </header>
42
+
43
+ <div class="card mb-8">
44
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">Global Session Settings</h2>
45
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
46
+ <div>
47
+ <label for="modelSelector" class="block text-sm font-medium text-gray-700">Main AI Model</label>
48
+ <select id="modelSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
49
+ <option>Loading models...</option>
50
+ </select>
51
+ </div>
52
+ <div>
53
+ <label for="contextSelector" class="block text-sm font-medium text-gray-700">Vocabulary Focus</label>
54
+ <select id="contextSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
55
+ <option value="General/Social">General/Social</option>
56
+ <option value="Professional/Business">Professional/Business</option>
57
+ <option value="Technical/IT">Technical/IT</option>
58
+ </select>
59
+ </div>
60
+ <div>
61
+ <label for="voiceSelector" class="block text-sm font-medium text-gray-700">Voice Accent (TTS)</label>
62
+ <select id="voiceSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
63
+ <option value="co.uk">British (Female)</option>
64
+ <option value="com">American (Female)</option>
65
+ <option value="com.au">Australian (Female)</option>
66
+ </select>
67
+ </div>
68
+ </div>
69
+ </div>
70
 
71
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
72
+ <!-- Coluna da Esquerda: Inputs -->
73
+ <div class="flex flex-col gap-6">
74
+ <div class="card">
75
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">1. Study from Text</h2>
76
+ <div id="textEditor" contenteditable="true" spellcheck="true" lang="en" class="w-full p-4 border rounded-md min-h-[150px]"></div>
77
+ <p class="text-sm text-right text-gray-500 mt-2">Characters: <span id="charCount">0</span>/10000</p>
78
+ <div class="mt-4 border-t pt-4 flex flex-wrap gap-2 justify-between items-center">
79
+ <button id="createCardButton" onclick="createFlashcardFromSelection()" class="btn btn-primary bg-green-600 hover:bg-green-700 disabled:opacity-50" disabled>+ Create Flashcard</button>
80
+ <button id="playButton" onclick="handlePlay()" class="btn btn-primary" disabled>
81
+ <span id="playText">Listen to Text</span>
82
+ <div id="playSpinner" class="spinner ml-2 hidden"></div>
83
+ </button>
84
  </div>
85
+ <audio id="audioPlayer" controls class="w-full mt-4 hidden"></audio>
86
  </div>
 
 
 
 
 
 
 
 
 
 
87
 
88
+ <div class="card">
89
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">2. Study from Image (Analysis)</h2>
90
+ <input type="file" id="imageUploader" accept="image/*" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:font-semibold file:bg-violet-50 file:text-violet-700 hover:file:bg-violet-100"/>
91
+ <img id="imagePreview" src="" class="mt-4 rounded-lg hidden max-h-60 mx-auto" alt="Preview"/>
92
+ <div id="imageAnalysisResult" class="mt-4"></div>
93
+ </div>
94
+
95
+ <div class="card">
96
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">3. Conversation Practice</h2>
97
+ <div id="chatDisplay" class="flex flex-col space-y-2 mb-4"></div>
98
+ <div class="flex gap-2">
99
+ <input type="text" id="chatInput" class="flex-grow border rounded-lg px-3 py-2" placeholder="Type or use the microphone...">
100
+ <button onclick="handleTextInput()" class="btn btn-primary bg-gray-500 hover:bg-gray-600">Send</button>
101
+ <button id="micButton" onclick="handleVoiceInput()" class="btn btn-primary mic-button w-12 h-12 rounded-full">🎙️</button>
102
+ </div>
103
+ </div>
104
 
105
+ <div class="card">
106
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">4. Image Generator (Nano Banana)</h2>
107
+ <p class="text-sm text-gray-600 mb-2">Describe a scene in English for the AI to draw.</p>
108
+ <div class="flex gap-2">
109
+ <input type="text" id="imagePromptInput" class="flex-grow border rounded-lg px-3 py-2" placeholder="Ex: a blue cat reading a book on the moon">
110
+ <button id="generateImageBtn" onclick="generateImage()" class="btn btn-primary">Generate</button>
111
+ </div>
112
+ <div id="imageResultContainer" class="mt-4 p-4 border rounded-lg bg-gray-50 min-h-[200px] flex items-center justify-center">
113
+ <p class="text-gray-500 italic">Your image will appear here.</p>
114
+ </div>
115
+ </div>
116
+
117
+ <div class="card">
118
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">5. Interactive Writing Practice</h2>
119
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
120
+ <div>
121
+ <label for="activityTypeSelector" class="block text-sm font-medium text-gray-700">Activity Type</label>
122
+ <select id="activityTypeSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
123
+ <option value="translate_pt_en">Translate (PT to EN)</option>
124
+ <option value="email_writing">Write Formal Email</option>
125
+ <option value="summary_writing">Summarize Main Text</option>
126
+ <option value="professional_conversation">Simulate Professional Conversation</option>
127
+ </select>
128
+ </div>
129
+ <div class="flex items-end">
130
+ <button id="generateActivityButton" onclick="generateActivity()" class="btn btn-primary w-full">Generate Activity</button>
131
+ </div>
132
+ </div>
133
+
134
+ <div id="activityChatContainer" class="mt-4 border-t pt-4">
135
+ <div id="activityChatDisplay">
136
+ <p class="text-gray-500 italic">Your interactive exercise will appear here.</p>
137
+ </div>
138
+ <div id="activityResponseArea" class="mt-4 hidden">
139
+ <label for="activityInput" class="block text-sm font-medium text-gray-700 mb-1">Your Answer:</label>
140
+ <textarea id="activityInput" rows="4" class="w-full p-2 border rounded-md"></textarea>
141
+ <button id="submitActivityBtn" onclick="submitActivityResponse()" class="btn btn-primary mt-2">Submit Answer</button>
142
+ </div>
143
+ </div>
144
+ </div>
145
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
+ <!-- Coluna da Direita: Flashcards -->
148
+ <div id="flashcardsPanel" class="card">
149
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">Your Study Session</h2>
150
+ <div id="flashcardList" class="space-y-6">
151
+ <p id="noCardsMessage" class="text-gray-500 italic">No flashcards created yet.</p>
 
 
 
 
 
 
 
 
152
  </div>
 
153
  </div>
 
 
 
 
 
 
 
 
 
 
154
  </div>
155
  </div>
 
 
 
 
 
156
 
 
 
157
  <script>
158
+ // --- ELEMENTOS GLOBAIS ---
159
+ const modelSelector = document.getElementById('modelSelector');
160
+ const contextSelector = document.getElementById('contextSelector');
161
+ const voiceSelector = document.getElementById('voiceSelector');
162
+ const textEditor = document.getElementById('textEditor');
163
+ const createCardButton = document.getElementById('createCardButton');
164
+ const playButton = document.getElementById('playButton');
165
+ const audioPlayer = document.getElementById('audioPlayer');
166
+ const flashcardList = document.getElementById('flashcardList');
167
+ const noCardsMessage = document.getElementById('noCardsMessage');
168
+ const imageUploader = document.getElementById('imageUploader');
169
+ const imagePreview = document.getElementById('imagePreview');
170
+ const imageAnalysisResult = document.getElementById('imageAnalysisResult');
171
+ const chatDisplay = document.getElementById('chatDisplay');
172
+ const chatInput = document.getElementById('chatInput');
173
+ const micButton = document.getElementById('micButton');
174
+ const imagePromptInput = document.getElementById('imagePromptInput');
175
+ const generateImageBtn = document.getElementById('generateImageBtn');
176
+ const imageResultContainer = document.getElementById('imageResultContainer');
177
+ const activityTypeSelector = document.getElementById('activityTypeSelector');
178
+ const generateActivityButton = document.getElementById('generateActivityButton');
179
+ const activityChatDisplay = document.getElementById('activityChatDisplay');
180
+ const activityResponseArea = document.getElementById('activityResponseArea');
181
+ const activityInput = document.getElementById('activityInput');
182
+ const submitActivityBtn = document.getElementById('submitActivityBtn');
183
+
184
+ let chatHistory = [];
185
+ let recognition;
186
+ let isRecording = false;
187
+ let currentActivityPrompt = "";
188
+
189
+ // --- CARREGAMENTO DINÂMICO DE MODELOS ---
190
+ async function populateModelSelector() {
191
+ try {
192
+ const response = await fetch('/list-models');
193
+ if (!response.ok) throw new Error('Failed to fetch model list.');
194
+ const models = await response.json();
195
+ modelSelector.innerHTML = '';
196
+ const geminiGroup = document.createElement('optgroup');
197
+ geminiGroup.label = 'Google Gemini';
198
+ const groqGroup = document.createElement('optgroup');
199
+ groqGroup.label = 'Groq (Ultra Fast)';
200
+ models.forEach(model => {
201
+ const option = document.createElement('option');
202
+ option.value = model.value;
203
+ option.textContent = model.name;
204
+ if (model.value.startsWith('gemini:')) {
205
+ geminiGroup.appendChild(option);
206
+ } else if (model.value.startsWith('groq:')) {
207
+ groqGroup.appendChild(option);
208
+ }
209
  });
210
+ modelSelector.appendChild(geminiGroup);
211
+ modelSelector.appendChild(groqGroup);
212
+ } catch (error) {
213
+ console.error("Error populating models:", error);
214
+ modelSelector.innerHTML = '<option>Error loading models</option>';
215
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  }
217
 
218
+ // --- FUNÇÕES DE ÁUDIO ---
219
+ async function fetchAudioBlob(text) {
220
+ const selectedTld = voiceSelector.value;
221
+ const response = await fetch('/tts-proxy', {
222
+ method: 'POST',
223
+ headers: { 'Content-Type': 'application/json' },
224
+ body: JSON.stringify({ text: text, tld: selectedTld })
225
+ });
226
+ if (!response.ok) {
227
+ const err = await response.json();
228
+ throw new Error(err.error || `HTTP error! status: ${response.status}`);
229
+ }
230
+ return await response.blob();
231
  }
232
+ async function playAudio(text, event) {
233
+ if(event) event.stopPropagation();
234
+ try {
235
+ const audioBlob = await fetchAudioBlob(text);
236
+ const audioUrl = URL.createObjectURL(audioBlob);
237
+ const audio = new Audio(audioUrl);
238
+ audio.play();
239
+ audio.onended = () => URL.revokeObjectURL(audioUrl);
240
+ } catch (error) { console.error('Error playing audio:', error); }
241
+ }
242
+
243
+ // --- LÓGICA DO EDITOR ---
244
+ const charCount = document.getElementById('charCount');
245
+ const playText = document.getElementById('playText');
246
+ const playSpinner = document.getElementById('playSpinner');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  textEditor.addEventListener('input', () => {
248
+ const len = textEditor.innerText.length;
249
+ charCount.textContent = `${len}/10000`;
250
+ playButton.disabled = len === 0;
251
  });
252
+ textEditor.addEventListener('mouseup', () => {
253
+ createCardButton.disabled = !window.getSelection().toString().trim();
 
 
 
 
 
 
 
 
 
 
 
254
  });
255
+ async function handlePlay() {
256
+ const text = textEditor.innerText;
257
+ if (!text) return;
258
+ playButton.disabled = true;
259
+ playText.textContent = 'Generating...';
260
+ playSpinner.classList.remove('hidden');
261
+ try {
262
+ const audioBlob = await fetchAudioBlob(text);
263
+ const audioUrl = URL.createObjectURL(audioBlob);
264
+ audioPlayer.src = audioUrl;
265
+ audioPlayer.classList.remove('hidden');
266
+ audioPlayer.play();
267
+ audioPlayer.onended = () => URL.revokeObjectURL(audioUrl);
268
+ } catch (error) {
269
+ alert(`Error generating audio: ${error.message}. Check the console for more details.`);
270
+ } finally {
271
+ playButton.disabled = false;
272
+ playText.textContent = 'Listen to Text';
273
+ playSpinner.classList.add('hidden');
274
+ }
275
+ }
276
+
277
+ // --- TODAS AS OUTRAS FUNÇÕES ---
278
+
279
+ // ATIVIDADES
280
+ async function generateActivity() {
281
+ const activityType = activityTypeSelector.value;
282
+ const context = contextSelector.value;
283
+ const sourceText = textEditor.innerText;
284
+ generateActivityButton.disabled = true;
285
+ generateActivityButton.innerHTML = '<div class="spinner mr-2"></div> Generating...';
286
+ activityChatDisplay.innerHTML = '<p class="text-gray-500 italic">Generating your exercise...</p>';
287
+ activityResponseArea.classList.add('hidden');
288
+ let prompt = "";
289
+ if (activityType === 'translate_pt_en') {
290
+ prompt = `Based on the '${context}' context, create one single sentence in Portuguese for me to translate into English.`;
291
+ } else if (activityType === 'email_writing') {
292
+ prompt = `Create a scenario for me to write a formal email, related to the '${context}' context. Give me clear instructions on what to write.`;
293
+ } else if (activityType === 'summary_writing') {
294
+ if (!sourceText) {
295
+ alert("Please paste text in the main editor to generate a summary activity.");
296
+ activityChatDisplay.innerHTML = '<p class="text-red-500">Please paste text in the editor in card #1 first.</p>';
297
+ generateActivityButton.disabled = false;
298
+ generateActivityButton.textContent = 'Generate Activity';
299
+ return;
300
+ }
301
+ prompt = `Your task is to summarize the following text in 2 or 3 sentences: "${sourceText}"`;
302
+ } else if (activityType === 'professional_conversation') {
303
+ prompt = `Start a professional role-playing conversation with me related to '${context}'. Ask me an open-ended question to begin.`;
304
+ }
305
+ try {
306
+ const response = await fetch('/explain-proxy', {
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json' },
309
+ body: JSON.stringify({
310
+ model: modelSelector.value,
311
+ context_focus: context,
312
+ custom_prompt: prompt
313
+ })
314
+ });
315
+ if (!response.ok) throw new Error((await response.json()).error);
316
+ const data = await response.json();
317
+ currentActivityPrompt = data.explanation;
318
+ activityChatDisplay.innerHTML = `<div class="ai-prompt"><strong>Your Task:</strong><p>${currentActivityPrompt}</p></div>`;
319
+ activityResponseArea.classList.remove('hidden');
320
+ } catch (error) {
321
+ activityChatDisplay.innerHTML = `<p class="text-red-600 font-semibold">Failed to generate activity: ${error.message}</p>`;
322
+ } finally {
323
+ generateActivityButton.disabled = false;
324
+ generateActivityButton.textContent = 'Generate Activity';
325
+ }
326
+ }
327
  async function submitActivityResponse() {
328
  const userResponse = activityInput.value.trim();
329
  if (!userResponse) {
 
450
  reader.readAsDataURL(file);
451
  });
452
  async function analyzeImage(base64Image) {
453
+ imageAnalysisResult.innerHTML = `<div class="flex items-center p-2"><div class="spinner !border-l-indigo-500 !border-gray-200"></div><p class="ml-3 text-gray-600">Analyzing image...</p></div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  try {
455
+ const response = await fetch('/analyze-image', {
456
  method: 'POST',
457
  headers: { 'Content-Type': 'application/json' },
458
  body: JSON.stringify({
459
  image: base64Image,
460
  model: modelSelector.value
461
  })
462
+ });
463
+ if (!response.ok) throw new Error((await response.json()).error);
464
+ const vocabulary = await response.json();
465
  displayImageVocabulary(vocabulary);
 
466
  } catch (error) {
467
+ imageAnalysisResult.innerHTML = `<p class="text-red-600 font-semibold">Analysis failed: ${error.message}</p>`;
 
 
 
 
 
468
  }
469
  }
470
  function displayImageVocabulary(vocabulary) {
471
+ imageAnalysisResult.innerHTML = '<h3 class="font-semibold mb-2">Suggested Vocabulary:</h3>';
472
+ const buttonContainer = document.createElement('div');
473
+ buttonContainer.className = 'flex flex-wrap gap-2';
474
+ vocabulary.forEach(({ term, definition }) => {
 
 
 
 
 
475
  const btn = document.createElement('button');
476
+ btn.className = 'btn bg-violet-100 text-violet-700 hover:bg-violet-200 text-sm';
477
+ btn.textContent = term;
478
+ btn.onclick = () => createFlashcardFromSuggestion(term, definition);
 
 
 
 
 
 
479
  buttonContainer.appendChild(btn);
480
  });
481
+ imageAnalysisResult.appendChild(buttonContainer);
482
  }
483
  async function generateImage() {
484
  const prompt = imagePromptInput.value.trim();
 
507
  }
508
 
509
  // FLASHCARDS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  async function createFlashcard(word, context, definition = null) {
 
 
 
 
 
 
 
 
 
 
511
  const btn = definition ? null : createCardButton;
512
+ if (btn) {
513
+ btn.disabled = true;
514
+ btn.innerHTML = '<div class="spinner mr-2"></div> Creating...';
515
+ }
516
  try {
517
+ const response = await fetch('/explain-proxy', {
518
  method: 'POST',
519
  headers: { 'Content-Type': 'application/json' },
520
  body: JSON.stringify({
521
+ word: word,
522
+ context: context,
523
  for_flashcard: true,
524
  model: modelSelector.value,
525
  context_focus: contextSelector.value
526
  })
527
+ });
528
+ if (!response.ok) {
529
+ const errData = await response.json();
530
+ throw new Error(errData.error || 'Unknown server error.');
531
+ }
532
+ const cardData = await response.json();
533
  if(definition && !cardData.definition) cardData.definition = definition;
 
 
 
534
  renderFlashcard(cardData);
 
 
 
 
 
 
 
535
  } catch (error) {
536
+ alert(`Failed to create flashcard: ${error.message}. Check API keys and model selection.`);
 
 
 
 
 
537
  } finally {
538
+ if (btn) {
539
+ btn.disabled = false;
540
+ btn.innerHTML = '+ Create Flashcard';
541
+ }
542
  }
543
  }
544
  function createFlashcardFromSelection() {
 
554
  const container = document.createElement('div');
555
  container.className = 'flashcard-container';
556
  container.id = cardId;
557
+ const minHeight = 380;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  container.style.minHeight = `${minHeight}px`;
559
  const fallback = 'Info not generated';
560
  container.innerHTML = `
561
+ <button class="delete-btn" onclick="deleteFlashcard('${cardId}', event)">🗑️</button>
562
+ <div class="flashcard-inner" style="min-height: ${minHeight}px;">
563
  <div class="flashcard-front">
564
  <div>
565
  <div class="text-sm text-gray-600 mb-2">Context Sentence:</div>
566
+ <p class="text-lg text-center text-gray-800">${data.gapped_sentence || fallback}</p>
567
  </div>
568
+ <div class="border-t pt-4 mt-4 text-center">
569
+ <strong class="text-indigo-600">Hint (PT):</strong> ${data.translation || fallback}
 
570
  </div>
571
+ <div class="text-xs text-center text-gray-400 mt-4">Click to see the answer</div>
572
  </div>
573
  <div class="flashcard-back">
574
  <div>
575
  <h3 class="text-xl font-bold text-center text-indigo-600 mb-2">${data.term || 'Term'}</h3>
576
+ <p class="flex items-center justify-center gap-2 text-gray-600 italic mb-3">
577
+ <button class="audio-btn" onclick="playAudio('${(data.context_sentence || '').replace(/'/g, "\\'")}', event)">🔊</button>
578
+ <span>${data.context_sentence || fallback}</span>
579
+ </p>
580
+ <p class="text-sm text-gray-800"><strong class="font-semibold">Definition:</strong> ${data.definition || fallback}</p>
 
 
581
  </div>
582
  <div class="border-t mt-3 pt-3">
583
  <h4 class="text-sm font-semibold text-center">Practice Your Pronunciation</h4>
584
  <div class="flex justify-center items-center gap-2 mt-2">
585
+ <button class="btn btn-primary w-12 h-12 rounded-full" onclick="handlePronunciationPractice(this, '${(data.context_sentence || '').replace(/'/g, "\\'")}', event)">🎙️</button>
586
  </div>
587
+ <div class="text-xs text-gray-600 p-2 mt-2 bg-gray-50 rounded-md min-h-[40px]" data-feedback-area></div>
588
  </div>
589
  </div>
590
  </div>`;
 
591
  flashcardList.prepend(container);
 
592
  container.addEventListener('click', (e) => {
593
  if(e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
594
  container.classList.toggle('flipped');
 
640
  };
641
  }
642
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
  // --- INICIALIZAÇÃO ---
644
  document.addEventListener('DOMContentLoaded', () => {
 
 
645
  populateModelSelector();
646
  initializeSpeechRecognition();
647
  });
648
  </script>
649
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
650
  </body>
651
  </html>
652