amauricunha commited on
Commit
d009447
Β·
verified Β·
1 Parent(s): 5d16dd6

Upload 10 files

Browse files
Files changed (10) hide show
  1. README.md +227 -10
  2. admin.html +1083 -0
  3. admin_module.py +524 -0
  4. app.py +1247 -0
  5. content_curator.py +409 -0
  6. database.py +744 -0
  7. index.html +1959 -0
  8. requeriments.txt +16 -0
  9. run.py +95 -0
  10. study_planner.py +460 -0
README.md CHANGED
@@ -1,12 +1,229 @@
1
- ---
2
- title: EnglishStudyHelper
3
- emoji: πŸ†
4
- colorFrom: blue
5
- colorTo: blue
6
- sdk: gradio
7
- sdk_version: 5.49.1
8
- app_file: app.py
9
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # πŸŽ“ English Helper - Advanced AI Language Learning Platform
2
+
3
+ ## 🌟 Visão Geral
4
+
5
+ O English Helper Γ© uma plataforma avanΓ§ada de aprendizado de inglΓͺs que combina inteligΓͺncia artificial, curadoria de conteΓΊdo personalizada, e planejamento de estudos inteligente para oferecer uma experiΓͺncia de aprendizado imersiva e personalizada.
6
+
7
+ ## πŸš€ Funcionalidades Principais
8
+
9
+ ### 1. πŸ€– Motor de Curadoria de ConteΓΊdo Personalizado
10
+ - **Busca Inteligente**: Encontre artigos e conteΓΊdos relevantes baseados em seus interesses
11
+ - **AnΓ‘lise de ConteΓΊdo com IA**: AvaliaΓ§Γ£o automΓ‘tica de dificuldade e relevΓ’ncia
12
+ - **Gerenciamento de Interesses**: Sistema dinΓ’mico de interesses do usuΓ‘rio
13
+ - **RecomendaΓ§Γ΅es Personalizadas**: SugestΓ΅es baseadas no nΓ­vel de proficiΓͺncia e contexto
14
+ - **Feeds RSS Integrados**: Acesso a fontes confiΓ‘veis de conteΓΊdo em inglΓͺs
15
+ - **ExtraΓ§Γ£o de Texto**: Processamento automΓ‘tico de URLs para extraΓ§Γ£o de conteΓΊdo
16
+
17
+ ### 2. 🎯 Ambiente de Imersão Interativo
18
+ - **Sistema de Flashcards**: CriaΓ§Γ£o e revisΓ£o de vocabulΓ‘rio personalizado
19
+ - **ConversaΓ§Γ£o com IA**: PrΓ‘tica de conversaΓ§Γ£o com feedback inteligente
20
+ - **ExercΓ­cios de Escrita**: PrΓ‘tica guiada com correΓ§Γ£o automΓ‘tica
21
+ - **GeraΓ§Γ£o de Imagens**: CriaΓ§Γ£o de imΓ‘genes contextuais para vocabulΓ‘rio
22
+ - **SΓ­ntese de Voz**: PronΓΊncia e Γ‘udio com Google Text-to-Speech
23
+ - **Contextos Personalizados**: AdaptaΓ§Γ£o para diferentes contextos (profissional, tΓ©cnico, social)
24
+
25
+ ### 3. πŸ“Š Planejador de Estudos e Painel de AnΓ‘lise
26
+ - **Planos de Estudo Personalizados**: Cronogramas adaptativos baseados em IA
27
+ - **AnΓ‘lise de Progresso**: MΓ©tricas detalhadas de desempenho
28
+ - **Sistema de Marcos**: Objetivos e checkpoints de progresso
29
+ - **RecomendaΓ§Γ΅es Adaptativas**: Ajustes automΓ‘ticos baseados no desempenho
30
+ - **Dashboard de Analytics**: VisualizaΓ§Γ΅es de progresso e estatΓ­sticas
31
+ - **Tracking de Atividades**: Monitoramento detalhado de todas as atividades
32
+
33
+ ## πŸ—οΈ Arquitetura TΓ©cnica
34
+
35
+ ### Backend (Python/Flask)
36
+ - **app.py**: AplicaΓ§Γ£o principal Flask com todas as rotas
37
+ - **database.py**: Gerenciamento de banco de dados SQLite
38
+ - **content_curator.py**: Motor de curadoria de conteΓΊdo
39
+ - **study_planner.py**: Sistema de planejamento de estudos com IA
40
+
41
+ ### Frontend (HTML/CSS/JavaScript)
42
+ - **index.html**: Interface responsiva e interativa
43
+ - **Design Moderno**: Interface limpa com Tailwind CSS
44
+ - **SPA Experience**: NavegaΓ§Γ£o fluida sem recarregamento
45
+
46
+ ### Banco de Dados
47
+ ```sql
48
+ -- Tabelas principais
49
+ users -- UsuΓ‘rios e autenticaΓ§Γ£o
50
+ user_settings -- ConfiguraΓ§Γ΅es personalizadas
51
+ flashcards -- Sistema de flashcards
52
+ study_sessions -- SessΓ΅es de estudo
53
+ user_articles -- Artigos salvos pelo usuΓ‘rio
54
+ user_interests -- Interesses do usuΓ‘rio
55
+ content_recommendations -- RecomendaΓ§Γ΅es de conteΓΊdo
56
+ study_plans -- Planos de estudo
57
+ study_plan_activities -- Atividades dos planos
58
+ user_analytics -- MΓ©tricas e analytics
59
+ ```
60
+
61
+ ### IntegraΓ§Γ΅es de IA
62
+ - **Groq**: Processamento de linguagem natural rΓ‘pido
63
+ - **Google Gemini**: AnΓ‘lise avanΓ§ada de conteΓΊdo
64
+ - **Google Text-to-Speech**: SΓ­ntese de voz
65
+ - **BeautifulSoup**: Web scraping inteligente
66
+ - **FeedParser**: Processamento de feeds RSS
67
+
68
+ ## πŸ› οΈ InstalaΓ§Γ£o e ConfiguraΓ§Γ£o
69
+
70
+ ### PrΓ©-requisitos
71
+ - Python 3.8+
72
+ - Chaves de API:
73
+ - `GROQ_API_KEY`: Para processamento de linguagem
74
+ - `GEMINI_API_KEY`: Para anΓ‘lise de conteΓΊdo
75
+ - `SECRET_KEY`: Para seguranΓ§a da sessΓ£o
76
+
77
+ ### InstalaΓ§Γ£o
78
+ ```bash
79
+ # Clone o repositΓ³rio
80
+ git clone <repository-url>
81
+ cd englishhelper
82
+
83
+ # Instale as dependΓͺncias
84
+ pip install -r requirements.txt
85
+
86
+ # Configure as variΓ‘veis de ambiente
87
+ export GROQ_API_KEY="sua-chave-groq"
88
+ export GEMINI_API_KEY="sua-chave-gemini"
89
+ export SECRET_KEY="sua-chave-secreta"
90
+
91
+ # Execute a aplicaΓ§Γ£o
92
+ python run.py
93
+ ```
94
+
95
+ ### ConfiguraΓ§Γ£o de Desenvolvimento
96
+ ```bash
97
+ # Para desenvolvimento
98
+ export FLASK_DEBUG=true
99
+ python app.py
100
+ ```
101
+
102
+ ## πŸ“š Como Usar
103
+
104
+ ### 1. CriaΓ§Γ£o de Conta
105
+ 1. Acesse a aplicaΓ§Γ£o no navegador
106
+ 2. Registre-se com email e senha
107
+ 3. Confirme seu email (opcional)
108
+ 4. Configure seu perfil (nΓ­vel, interesses, contexto)
109
+
110
+ ### 2. Curadoria de ConteΓΊdo
111
+ 1. **Buscar ConteΓΊdo**: Use a barra de busca para encontrar artigos
112
+ 2. **Salvar Artigos**: Clique em "Salvar" para adicionar Γ  sua biblioteca
113
+ 3. **Gerenciar Interesses**: Configure seus tΓ³picos de interesse
114
+ 4. **RecomendaΓ§Γ΅es**: Receba sugestΓ΅es personalizadas baseadas em IA
115
+
116
+ ### 3. Estudo Interativo
117
+ 1. **Flashcards**: Crie e revise vocabulΓ‘rio
118
+ 2. **ConversaΓ§Γ£o**: Pratique com o bot de IA
119
+ 3. **Escrita**: Complete exercΓ­cios guiados
120
+ 4. **Imagens**: Gere imagens para contextualizar vocabulΓ‘rio
121
+
122
+ ### 4. Planejamento de Estudos
123
+ 1. **Gerar Plano**: Configure seu nΓ­vel atual e objetivo
124
+ 2. **Seguir Atividades**: Complete as atividades sugeridas
125
+ 3. **Marcar Progresso**: Registre suas atividades concluΓ­das
126
+ 4. **Acompanhar Analytics**: Monitore seu progresso
127
+
128
+ ## 🎯 Níveis Suportados
129
+
130
+ O sistema suporta todos os nΓ­veis do Quadro Europeu Comum:
131
+ - **A1**: BΓ‘sico - vocabulΓ‘rio essencial e frases simples
132
+ - **A2**: Elementar - situaΓ§Γ΅es cotidianas e tempos verbais bΓ‘sicos
133
+ - **B1**: IntermediΓ‘rio - conversas mais complexas e opiniΓ΅es
134
+ - **B2**: IntermediΓ‘rio Superior - textos complexos e apresentaΓ§Γ΅es
135
+ - **C1**: AvanΓ§ado - textos acadΓͺmicos e expressΓ΅es sofisticadas
136
+ - **C2**: ProficiΓͺncia - fluΓͺncia nativa e literatura
137
+
138
+ ## πŸ”§ Contextos de Aprendizado
139
+
140
+ ### Profissional/NegΓ³cios
141
+ - VocabulΓ‘rio corporativo
142
+ - ApresentaΓ§Γ΅es e relatΓ³rios
143
+ - ComunicaΓ§Γ£o formal
144
+ - Networking e reuniΓ΅es
145
+
146
+ ### TΓ©cnico/TI
147
+ - Terminologia tΓ©cnica
148
+ - DocumentaΓ§Γ£o especializada
149
+ - ComunicaΓ§Γ£o em equipes de desenvolvimento
150
+ - Artigos de tecnologia
151
+
152
+ ### Geral/Social
153
+ - ConversaΓ§Γ£o cotidiana
154
+ - MΓ­dias sociais
155
+ - Entretenimento
156
+ - Viagens e cultura
157
+
158
+ ## πŸ“ˆ Analytics e MΓ©tricas
159
+
160
+ ### MΓ©tricas Individuais
161
+ - Tempo total de estudo
162
+ - Palavras aprendidas
163
+ - Artigos lidos
164
+ - Atividades completadas
165
+ - Progresso por nΓ­vel
166
+
167
+ ### VisualizaΓ§Γ΅es
168
+ - GrΓ‘ficos de progresso temporal
169
+ - DistribuiΓ§Γ£o de atividades
170
+ - ComparaΓ§Γ£o de performance
171
+ - TendΓͺncias de aprendizado
172
+
173
+ ## πŸš€ Funcionalidades AvanΓ§adas
174
+
175
+ ### Sistema de RecomendaΓ§Γ΅es
176
+ - AnΓ‘lise de padrΓ΅es de aprendizado
177
+ - SugestΓ΅es adaptativas de conteΓΊdo
178
+ - PersonalizaΓ§Γ£o baseada em performance
179
+ - Machine Learning para otimizaΓ§Γ£o
180
+
181
+ ### Planejamento Inteligente
182
+ - Cronogramas adaptativos
183
+ - Balanceamento de habilidades
184
+ - Ajustes baseados em progresso
185
+ - Marcos e objetivos personalizados
186
+
187
+ ### Curadoria AutomΓ‘tica
188
+ - Busca em tempo real
189
+ - Filtros de qualidade
190
+ - AnΓ‘lise de relevΓ’ncia
191
+ - DiversificaΓ§Γ£o de fontes
192
+
193
+ ## πŸ” SeguranΓ§a e Privacidade
194
+
195
+ - AutenticaΓ§Γ£o segura com hash de senhas
196
+ - SessΓ΅es criptografadas
197
+ - Dados locais em SQLite
198
+ - Sem compartilhamento de dados pessoais
199
+ - Conformidade com boas prΓ‘ticas de seguranΓ§a
200
+
201
+ ## πŸš€ Roadmap Futuro
202
+
203
+ ### PrΓ³ximas Funcionalidades
204
+ - [ ] IntegraΓ§Γ£o com mais APIs de IA
205
+ - [ ] Sistema de gamificaΓ§Γ£o
206
+ - [ ] Comunidade de learners
207
+ - [ ] CertificaΓ§Γ΅es integradas
208
+ - [ ] App mΓ³vel
209
+ - [ ] IntegraΓ§Γ£o com calendΓ‘rios
210
+ - [ ] Suporte a mΓΊltiplos idiomas
211
+
212
+ ### Melhorias TΓ©cnicas
213
+ - [ ] Cache inteligente
214
+ - [ ] OtimizaΓ§Γ£o de performance
215
+ - [ ] Testes automatizados
216
+ - [ ] Deploy em cloud
217
+ - [ ] Monitoramento avanΓ§ado
218
+
219
+ ## 🀝 Contribuição
220
+
221
+ Este Γ© um projeto de demonstraΓ§Γ£o das capacidades avanΓ§adas de IA para educaΓ§Γ£o. SugestΓ΅es e melhorias sΓ£o bem-vindas!
222
+
223
+ ## πŸ“„ LicenΓ§a
224
+
225
+ Projeto desenvolvido para fins educacionais e demonstraΓ§Γ£o de tecnologias avanΓ§adas de IA aplicadas ao aprendizado de idiomas.
226
+
227
  ---
228
 
229
+ **English Helper** - Transformando o aprendizado de inglΓͺs com inteligΓͺncia artificial! πŸŽ“βœ¨
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>
admin_module.py ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # admin_module.py - Administrative interface for English Helper
2
+ import os
3
+ import json
4
+ import hashlib
5
+ from datetime import datetime, timedelta
6
+ from functools import wraps
7
+ from flask import session, request, jsonify, redirect, url_for
8
+ import sqlite3
9
+ from database import get_db_connection
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class AdminManager:
15
+ def __init__(self):
16
+ self.admin_credentials = self._load_admin_credentials()
17
+ self.token_costs = {
18
+ 'groq': {'input': 0.00000059, 'output': 0.00000079}, # per token
19
+ 'gemini': {'input': 0.00000125, 'output': 0.00000375} # per token
20
+ }
21
+
22
+ def _load_admin_credentials(self):
23
+ """Load admin credentials from environment variables (Hugging Face secrets)"""
24
+ try:
25
+ # Try to load from Hugging Face secrets format
26
+ admin_user = os.environ.get('ADMIN_USERNAME', 'admin')
27
+ admin_pass = os.environ.get('ADMIN_PASSWORD', 'admin123')
28
+
29
+ # For security, hash the password
30
+ admin_pass_hash = hashlib.sha256(admin_pass.encode()).hexdigest()
31
+
32
+ return {
33
+ 'username': admin_user,
34
+ 'password_hash': admin_pass_hash,
35
+ 'original_password': admin_pass # Store for initial comparison
36
+ }
37
+ except Exception as e:
38
+ logger.error(f"Error loading admin credentials: {e}")
39
+ # Fallback credentials
40
+ return {
41
+ 'username': 'admin',
42
+ 'password_hash': hashlib.sha256('admin123'.encode()).hexdigest(),
43
+ 'original_password': 'admin123'
44
+ }
45
+
46
+ def authenticate_admin(self, username, password):
47
+ """Authenticate admin user"""
48
+ try:
49
+ if username != self.admin_credentials['username']:
50
+ return False
51
+
52
+ # Check password hash
53
+ password_hash = hashlib.sha256(password.encode()).hexdigest()
54
+ return password_hash == self.admin_credentials['password_hash']
55
+ except Exception as e:
56
+ logger.error(f"Admin authentication error: {e}")
57
+ return False
58
+
59
+ def is_admin_logged_in(self):
60
+ """Check if admin is logged in"""
61
+ return session.get('admin_authenticated', False)
62
+
63
+ def login_admin(self, username, password):
64
+ """Admin login"""
65
+ if self.authenticate_admin(username, password):
66
+ session['admin_authenticated'] = True
67
+ session['admin_username'] = username
68
+ session['admin_login_time'] = datetime.now().isoformat()
69
+ return True
70
+ return False
71
+
72
+ def logout_admin(self):
73
+ """Admin logout"""
74
+ session.pop('admin_authenticated', None)
75
+ session.pop('admin_username', None)
76
+ session.pop('admin_login_time', None)
77
+
78
+ def get_system_stats(self):
79
+ """Get comprehensive system statistics"""
80
+ try:
81
+ conn = get_db_connection()
82
+ cursor = conn.cursor()
83
+
84
+ # User statistics
85
+ cursor.execute("SELECT COUNT(*) FROM users")
86
+ total_users = cursor.fetchone()[0]
87
+
88
+ cursor.execute("SELECT COUNT(*) FROM users WHERE created_at > datetime('now', '-7 days')")
89
+ new_users_week = cursor.fetchone()[0]
90
+
91
+ cursor.execute("SELECT COUNT(*) FROM users WHERE created_at > datetime('now', '-1 day')")
92
+ new_users_today = cursor.fetchone()[0]
93
+
94
+ # Activity statistics
95
+ cursor.execute("SELECT COUNT(*) FROM study_sessions")
96
+ total_sessions = cursor.fetchone()[0]
97
+
98
+ cursor.execute("SELECT COUNT(*) FROM flashcards")
99
+ total_flashcards = cursor.fetchone()[0]
100
+
101
+ cursor.execute("SELECT COUNT(*) FROM user_articles")
102
+ total_articles = cursor.fetchone()[0]
103
+
104
+ cursor.execute("SELECT COUNT(*) FROM study_plans")
105
+ total_study_plans = cursor.fetchone()[0]
106
+
107
+ # Token usage statistics
108
+ cursor.execute("SELECT SUM(tokens_used), COUNT(*) FROM token_usage")
109
+ token_stats = cursor.fetchone()
110
+ total_tokens = token_stats[0] if token_stats[0] else 0
111
+ total_api_calls = token_stats[1] if token_stats[1] else 0
112
+
113
+ # Calculate estimated costs
114
+ estimated_cost = self._calculate_estimated_cost(cursor)
115
+
116
+ # Recent activity
117
+ cursor.execute("""
118
+ SELECT u.email, s.created_at, s.activity_type
119
+ FROM study_sessions s
120
+ JOIN users u ON s.user_id = u.id
121
+ ORDER BY s.created_at DESC
122
+ LIMIT 10
123
+ """)
124
+ recent_activity = cursor.fetchall()
125
+
126
+ conn.close()
127
+
128
+ return {
129
+ 'users': {
130
+ 'total': total_users,
131
+ 'new_week': new_users_week,
132
+ 'new_today': new_users_today
133
+ },
134
+ 'activity': {
135
+ 'total_sessions': total_sessions,
136
+ 'total_flashcards': total_flashcards,
137
+ 'total_articles': total_articles,
138
+ 'total_study_plans': total_study_plans
139
+ },
140
+ 'api_usage': {
141
+ 'total_tokens': total_tokens,
142
+ 'total_calls': total_api_calls,
143
+ 'estimated_cost': estimated_cost
144
+ },
145
+ 'recent_activity': [
146
+ {
147
+ 'user': activity[0],
148
+ 'timestamp': activity[1],
149
+ 'activity': activity[2]
150
+ } for activity in recent_activity
151
+ ]
152
+ }
153
+ except Exception as e:
154
+ logger.error(f"Error getting system stats: {e}")
155
+ return {}
156
+
157
+ def _calculate_estimated_cost(self, cursor):
158
+ """Calculate estimated API costs"""
159
+ try:
160
+ cursor.execute("""
161
+ SELECT api_provider, SUM(input_tokens), SUM(output_tokens)
162
+ FROM token_usage
163
+ GROUP BY api_provider
164
+ """)
165
+ usage_by_provider = cursor.fetchall()
166
+
167
+ total_cost = 0
168
+ for provider, input_tokens, output_tokens in usage_by_provider:
169
+ if provider in self.token_costs:
170
+ costs = self.token_costs[provider]
171
+ total_cost += (input_tokens * costs['input']) + (output_tokens * costs['output'])
172
+
173
+ return round(total_cost, 4)
174
+ except:
175
+ return 0
176
+
177
+ def get_all_users(self, page=1, per_page=20):
178
+ """Get paginated list of all users"""
179
+ try:
180
+ conn = get_db_connection()
181
+ cursor = conn.cursor()
182
+
183
+ offset = (page - 1) * per_page
184
+
185
+ cursor.execute("""
186
+ SELECT u.id, u.email, u.created_at, u.email_confirmed, u.last_login,
187
+ COUNT(DISTINCT s.id) as session_count,
188
+ COUNT(DISTINCT f.id) as flashcard_count,
189
+ COUNT(DISTINCT a.id) as article_count
190
+ FROM users u
191
+ LEFT JOIN study_sessions s ON u.id = s.user_id
192
+ LEFT JOIN flashcards f ON u.id = f.user_id
193
+ LEFT JOIN user_articles a ON u.id = a.user_id
194
+ GROUP BY u.id
195
+ ORDER BY u.created_at DESC
196
+ LIMIT ? OFFSET ?
197
+ """, (per_page, offset))
198
+
199
+ users = cursor.fetchall()
200
+
201
+ # Get total count
202
+ cursor.execute("SELECT COUNT(*) FROM users")
203
+ total_users = cursor.fetchone()[0]
204
+
205
+ conn.close()
206
+
207
+ return {
208
+ 'users': [
209
+ {
210
+ 'id': user[0],
211
+ 'email': user[1],
212
+ 'created_at': user[2],
213
+ 'email_confirmed': bool(user[3]),
214
+ 'last_login': user[4],
215
+ 'session_count': user[5],
216
+ 'flashcard_count': user[6],
217
+ 'article_count': user[7]
218
+ } for user in users
219
+ ],
220
+ 'total': total_users,
221
+ 'page': page,
222
+ 'per_page': per_page,
223
+ 'total_pages': (total_users + per_page - 1) // per_page
224
+ }
225
+ except Exception as e:
226
+ logger.error(f"Error getting users: {e}")
227
+ return {'users': [], 'total': 0}
228
+
229
+ def delete_user(self, user_id):
230
+ """Delete a user and all associated data"""
231
+ try:
232
+ conn = get_db_connection()
233
+ cursor = conn.cursor()
234
+
235
+ # Delete in order to respect foreign key constraints
236
+ tables = [
237
+ 'study_plan_activities', 'study_plans', 'user_analytics',
238
+ 'content_recommendations', 'user_interests', 'user_articles',
239
+ 'study_sessions', 'flashcards', 'user_settings', 'users'
240
+ ]
241
+
242
+ for table in tables:
243
+ cursor.execute(f"DELETE FROM {table} WHERE user_id = ?", (user_id,))
244
+
245
+ conn.commit()
246
+ conn.close()
247
+
248
+ return True
249
+ except Exception as e:
250
+ logger.error(f"Error deleting user {user_id}: {e}")
251
+ return False
252
+
253
+ def get_user_details(self, user_id):
254
+ """Get detailed information about a specific user"""
255
+ try:
256
+ conn = get_db_connection()
257
+ cursor = conn.cursor()
258
+
259
+ # Basic user info
260
+ cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
261
+ user = cursor.fetchone()
262
+
263
+ if not user:
264
+ return None
265
+
266
+ # User settings
267
+ cursor.execute("SELECT * FROM user_settings WHERE user_id = ?", (user_id,))
268
+ settings = cursor.fetchone()
269
+
270
+ # Recent activity
271
+ cursor.execute("""
272
+ SELECT activity_type, created_at, duration_minutes
273
+ FROM study_sessions
274
+ WHERE user_id = ?
275
+ ORDER BY created_at DESC
276
+ LIMIT 20
277
+ """, (user_id,))
278
+ recent_sessions = cursor.fetchall()
279
+
280
+ # Token usage
281
+ cursor.execute("""
282
+ SELECT api_provider, SUM(input_tokens), SUM(output_tokens), COUNT(*)
283
+ FROM token_usage
284
+ WHERE user_id = ?
285
+ GROUP BY api_provider
286
+ """, (user_id,))
287
+ token_usage = cursor.fetchall()
288
+
289
+ conn.close()
290
+
291
+ return {
292
+ 'user': {
293
+ 'id': user[0],
294
+ 'email': user[1],
295
+ 'created_at': user[2],
296
+ 'email_confirmed': bool(user[3]),
297
+ 'last_login': user[4]
298
+ },
299
+ 'settings': dict(zip([col[0] for col in cursor.description], settings)) if settings else {},
300
+ 'recent_sessions': [
301
+ {
302
+ 'activity': session[0],
303
+ 'timestamp': session[1],
304
+ 'duration': session[2]
305
+ } for session in recent_sessions
306
+ ],
307
+ 'token_usage': [
308
+ {
309
+ 'provider': usage[0],
310
+ 'input_tokens': usage[1],
311
+ 'output_tokens': usage[2],
312
+ 'calls': usage[3]
313
+ } for usage in token_usage
314
+ ]
315
+ }
316
+ except Exception as e:
317
+ logger.error(f"Error getting user details for {user_id}: {e}")
318
+ return None
319
+
320
+ def record_token_usage(self, user_id, api_provider, input_tokens, output_tokens, operation_type):
321
+ """Record token usage for cost tracking"""
322
+ try:
323
+ conn = get_db_connection()
324
+ cursor = conn.cursor()
325
+
326
+ cursor.execute("""
327
+ INSERT INTO token_usage
328
+ (user_id, api_provider, input_tokens, output_tokens, operation_type, created_at)
329
+ VALUES (?, ?, ?, ?, ?, ?)
330
+ """, (user_id, api_provider, input_tokens, output_tokens, operation_type, datetime.now().isoformat()))
331
+
332
+ conn.commit()
333
+ conn.close()
334
+
335
+ return True
336
+ except Exception as e:
337
+ logger.error(f"Error recording token usage: {e}")
338
+ return False
339
+
340
+ def get_database_schema(self):
341
+ """Get database schema information"""
342
+ try:
343
+ conn = get_db_connection()
344
+ cursor = conn.cursor()
345
+
346
+ # Get all tables
347
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
348
+ tables = cursor.fetchall()
349
+
350
+ schema_info = {}
351
+ for table in tables:
352
+ table_name = table[0]
353
+
354
+ # Get table info
355
+ cursor.execute(f"PRAGMA table_info({table_name})")
356
+ columns = cursor.fetchall()
357
+
358
+ # Get row count
359
+ cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
360
+ row_count = cursor.fetchone()[0]
361
+
362
+ schema_info[table_name] = {
363
+ 'columns': [
364
+ {
365
+ 'name': col[1],
366
+ 'type': col[2],
367
+ 'not_null': bool(col[3]),
368
+ 'primary_key': bool(col[5])
369
+ } for col in columns
370
+ ],
371
+ 'row_count': row_count
372
+ }
373
+
374
+ conn.close()
375
+ return schema_info
376
+ except Exception as e:
377
+ logger.error(f"Error getting database schema: {e}")
378
+ return {}
379
+
380
+ def get_system_health(self):
381
+ """Get system health metrics"""
382
+ try:
383
+ import psutil
384
+ import os
385
+
386
+ # Memory usage
387
+ memory = psutil.virtual_memory()
388
+
389
+ # Disk usage
390
+ disk = psutil.disk_usage('/')
391
+
392
+ # Database size
393
+ db_path = 'data/englishhelper.db'
394
+ db_size = os.path.getsize(db_path) if os.path.exists(db_path) else 0
395
+
396
+ # Recent error logs (would implement proper logging)
397
+ recent_errors = self._get_recent_errors()
398
+
399
+ return {
400
+ 'memory': {
401
+ 'total': memory.total,
402
+ 'used': memory.used,
403
+ 'available': memory.available,
404
+ 'percent': memory.percent
405
+ },
406
+ 'disk': {
407
+ 'total': disk.total,
408
+ 'used': disk.used,
409
+ 'free': disk.free,
410
+ 'percent': disk.percent
411
+ },
412
+ 'database': {
413
+ 'size_bytes': db_size,
414
+ 'size_mb': round(db_size / 1024 / 1024, 2)
415
+ },
416
+ 'recent_errors': recent_errors,
417
+ 'uptime': self._get_uptime()
418
+ }
419
+ except Exception as e:
420
+ logger.error(f"Error getting system health: {e}")
421
+ return {}
422
+
423
+ def _get_recent_errors(self):
424
+ """Get recent error logs (simplified)"""
425
+ try:
426
+ # This would typically read from log files
427
+ # For now, return sample data
428
+ return [
429
+ {
430
+ 'timestamp': '2024-10-11 14:30:00',
431
+ 'level': 'ERROR',
432
+ 'message': 'API rate limit exceeded for user 123',
433
+ 'module': 'groq_client'
434
+ },
435
+ {
436
+ 'timestamp': '2024-10-11 13:45:00',
437
+ 'level': 'WARNING',
438
+ 'message': 'High memory usage detected',
439
+ 'module': 'system_monitor'
440
+ }
441
+ ]
442
+ except:
443
+ return []
444
+
445
+ def _get_uptime(self):
446
+ """Get system uptime"""
447
+ try:
448
+ import psutil
449
+ boot_time = psutil.boot_time()
450
+ uptime_seconds = datetime.now().timestamp() - boot_time
451
+
452
+ days = int(uptime_seconds // 86400)
453
+ hours = int((uptime_seconds % 86400) // 3600)
454
+ minutes = int((uptime_seconds % 3600) // 60)
455
+
456
+ return f"{days}d {hours}h {minutes}m"
457
+ except:
458
+ return "Unknown"
459
+
460
+ def check_system_alerts(self):
461
+ """Check for system alerts and warnings"""
462
+ alerts = []
463
+
464
+ try:
465
+ # Check token usage limits
466
+ conn = get_db_connection()
467
+ cursor = conn.cursor()
468
+
469
+ # Check daily token usage
470
+ cursor.execute("""
471
+ SELECT SUM(tokens_used)
472
+ FROM token_usage
473
+ WHERE date(created_at) = date('now')
474
+ """)
475
+ daily_tokens = cursor.fetchone()[0] or 0
476
+
477
+ if daily_tokens > 100000: # Alert threshold
478
+ alerts.append({
479
+ 'type': 'warning',
480
+ 'message': f'High daily token usage: {daily_tokens:,} tokens',
481
+ 'action': 'Monitor API costs'
482
+ })
483
+
484
+ # Check error rates
485
+ cursor.execute("""
486
+ SELECT COUNT(*) FROM token_usage
487
+ WHERE created_at > datetime('now', '-1 hour')
488
+ """)
489
+ hourly_requests = cursor.fetchone()[0] or 0
490
+
491
+ if hourly_requests > 500: # High load threshold
492
+ alerts.append({
493
+ 'type': 'info',
494
+ 'message': f'High API request rate: {hourly_requests} requests/hour',
495
+ 'action': 'Monitor performance'
496
+ })
497
+
498
+ # Check database size
499
+ health = self.get_system_health()
500
+ if health.get('database', {}).get('size_mb', 0) > 100: # 100MB threshold
501
+ alerts.append({
502
+ 'type': 'warning',
503
+ 'message': f'Large database size: {health["database"]["size_mb"]}MB',
504
+ 'action': 'Consider archiving old data'
505
+ })
506
+
507
+ conn.close()
508
+ return alerts
509
+
510
+ except Exception as e:
511
+ logger.error(f"Error checking system alerts: {e}")
512
+ return []
513
+
514
+ # Decorator for admin-only routes
515
+ def admin_required(f):
516
+ @wraps(f)
517
+ def decorated_function(*args, **kwargs):
518
+ if not admin_manager.is_admin_logged_in():
519
+ return jsonify({'error': 'Admin authentication required'}), 401
520
+ return f(*args, **kwargs)
521
+ return decorated_function
522
+
523
+ # Global admin manager instance
524
+ admin_manager = AdminManager()
app.py ADDED
@@ -0,0 +1,1247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #app.py
2
+ import os
3
+ import io
4
+ import json
5
+ import base64
6
+ from datetime import datetime
7
+ from PIL import Image
8
+ from email_validator import validate_email, EmailNotValidError
9
+
10
+ from flask import Flask, request, jsonify, send_file, session, render_template_string, redirect, url_for, Response
11
+ from flask_session import Session
12
+ from gtts import gTTS
13
+ from groq import Groq
14
+ import google.generativeai as genai
15
+ from google.generativeai.types import GenerationConfig
16
+
17
+ # Import database functions
18
+ from database import (
19
+ init_db, close_db, create_user, authenticate_user, confirm_email,
20
+ get_user_settings, update_user_settings, save_user_flashcard,
21
+ get_user_flashcards, record_study_session, login_required,
22
+ get_current_user, send_confirmation_email, save_user_article,
23
+ get_user_articles, update_user_interests, get_user_interests,
24
+ create_study_plan, get_user_study_plans, add_study_activity,
25
+ get_study_activities, record_analytics_metric, get_user_analytics,
26
+ get_db_connection
27
+ )
28
+
29
+ # Import content curation and study planner
30
+ from content_curator import content_curator
31
+ from study_planner import study_planner
32
+ from admin_module import admin_manager, admin_required
33
+
34
+ # --- CONFIGURAÇÃO INICIAL ---
35
+ app = Flask(__name__)
36
+
37
+ # Configuration for sessions
38
+ app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-change-in-production')
39
+ app.config['SESSION_TYPE'] = 'filesystem'
40
+ app.config['SESSION_PERMANENT'] = False
41
+ app.config['SESSION_USE_SIGNER'] = True
42
+ app.config['SESSION_KEY_PREFIX'] = 'englishhelper:'
43
+
44
+ Session(app)
45
+
46
+ # Initialize database
47
+ def initialize_database():
48
+ init_db()
49
+
50
+ # Initialize on startup
51
+ initialize_database()
52
+
53
+ # Token tracking helper function
54
+ def track_token_usage(user_id, provider, input_tokens, output_tokens, operation):
55
+ """Helper function to track token usage"""
56
+ try:
57
+ admin_manager.record_token_usage(user_id, provider, input_tokens, output_tokens, operation)
58
+ except Exception as e:
59
+ print(f"Token tracking error: {e}")
60
+
61
+ @app.teardown_appcontext
62
+ def close_database(error):
63
+ close_db(error)
64
+
65
+ # --- CONFIGURAÇÃO DAS APIS LLM ---
66
+ genai_client = None
67
+ groq_client = None
68
+
69
+ # 1. ConfiguraΓ§Γ£o Gemini
70
+ try:
71
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
72
+ if GEMINI_API_KEY:
73
+ genai.configure(api_key=GEMINI_API_KEY)
74
+ genai_client = genai
75
+ else:
76
+ print("AVISO: GEMINI_API_KEY nΓ£o configurada.")
77
+ except Exception as e:
78
+ genai_client = None
79
+ print(f"ERRO ao inicializar o cliente Gemini: {e}.")
80
+
81
+ # 2. ConfiguraΓ§Γ£o Groq
82
+ try:
83
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
84
+ if GROQ_API_KEY:
85
+ groq_client = Groq(api_key=GROQ_API_KEY)
86
+ else:
87
+ print("AVISO: GROQ_API_KEY nΓ£o configurada.")
88
+ except Exception as e:
89
+ groq_client = None
90
+ print(f"ERRO ao inicializar o cliente Groq: {e}.")
91
+
92
+
93
+ # --- ROTA PARA LISTAR MODELOS DINAMICAMENTE ---
94
+ @app.route('/list-models')
95
+ def list_models():
96
+ available_models = []
97
+ groq_text_models = [
98
+ "llama-3.1-8b-instant",
99
+ "llama-3.3-70b-versatile",
100
+ "openai/gpt-oss-120b",
101
+ "openai/gpt-oss-20b"
102
+ ]
103
+ try:
104
+ if genai_client:
105
+ for m in genai_client.list_models():
106
+ if 'generateContent' in m.supported_generation_methods:
107
+ model_name = m.name.replace("models/", "")
108
+ if "flash" in model_name or "pro" in model_name:
109
+ available_models.append({
110
+ "value": f"gemini:{model_name}",
111
+ "name": m.display_name
112
+ })
113
+ if groq_client:
114
+ for model_id in groq_text_models:
115
+ display_name = model_id.split('/')[-1].replace('-instant', '').replace('-versatile', '')
116
+ available_models.append({
117
+ "value": f"groq:{model_id}",
118
+ "name": f"Groq: {display_name}"
119
+ })
120
+ except Exception as e:
121
+ print(f"Erro ao listar modelos: {e}")
122
+ return jsonify([
123
+ {"value": "gemini:gemini-2.5-flash-latest", "name": "Gemini 2.5 Flash (Fallback)"},
124
+ {"value": "groq:llama-3.1-8b-instant", "name": "Llama 3.1 8B (Fallback)"}
125
+ ])
126
+ return jsonify(available_models)
127
+
128
+
129
+ # --- ROTAS PRINCIPAIS ---
130
+
131
+ @app.route('/tts-proxy', methods=['POST'])
132
+ def tts_proxy():
133
+ data = request.get_json()
134
+ text = data.get('text', '')
135
+ tld = data.get('tld', 'co.uk')
136
+ if not text: return jsonify({"error": "No text provided"}), 400
137
+ try:
138
+ tts = gTTS(text=text, lang='en', tld=tld)
139
+ mp3_fp = io.BytesIO()
140
+ tts.write_to_fp(mp3_fp)
141
+ mp3_fp.seek(0)
142
+ return send_file(mp3_fp, mimetype='audio/mpeg')
143
+ except Exception as e:
144
+ return jsonify({"error": f"Failed to generate audio via gTTS: {e}"}), 500
145
+
146
+ @app.route('/explain-proxy', methods=['POST'])
147
+ def explain_proxy():
148
+ data = request.get_json()
149
+ model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1)
150
+ context_focus = data.get('context_focus', 'General/Social')
151
+ custom_prompt = data.get('custom_prompt', None)
152
+ word = data.get('word', '').strip()
153
+ context = data.get('context', '')
154
+ for_flashcard = data.get('for_flashcard', False)
155
+
156
+ if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client):
157
+ return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503
158
+
159
+ system_instruction_base = f"You are a professional English tutor. The user's study focus is '{context_focus}'. All your responses must be in ENGLISH."
160
+ try:
161
+ if custom_prompt:
162
+ activity_text = get_ai_text_response(model_provider, model_name, system_instruction_base, custom_prompt)
163
+ return jsonify({"explanation": activity_text})
164
+
165
+ if not word: return jsonify({"error": "No word selected."}), 400
166
+
167
+ if for_flashcard:
168
+ schema = {"type": "object", "properties": {"term": {"type": "string"}, "translation": {"type": "string"}, "context_sentence": {"type": "string"}, "gapped_sentence": {"type": "string"}, "definition": {"type": "string"}}, "required": ["term", "translation", "context_sentence", "gapped_sentence", "definition"]}
169
+ prompt = f"Analyze '{word}' in context: '{context}'. Generate a JSON for a flashcard. The 'gapped_sentence' must replace '{word}' with '______________'."
170
+ return jsonify(get_ai_text_response(model_provider, model_name, system_instruction_base, prompt, json_schema=schema))
171
+ else:
172
+ prompt = f"Analyze '{word}' in context: '{context}'. Provide a one-sentence English explanation, then '---', then the Portuguese translation."
173
+ parts = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt).split('---', 1)
174
+ return jsonify({"explanation": parts[0].strip(), "translation": parts[1].strip() if len(parts) > 1 else 'N/A'})
175
+ except Exception as e:
176
+ print(f"AI ANALYSIS ERROR in /explain-proxy: {e}")
177
+ return jsonify({"error": f"AI analysis failed: {e}"}), 500
178
+
179
+ @app.route('/activity-feedback', methods=['POST'])
180
+ def activity_feedback():
181
+ data = request.get_json()
182
+ model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1)
183
+ context_focus = data.get('context_focus', 'General/Social')
184
+ original_prompt = data.get('original_prompt', '')
185
+ user_response = data.get('user_response', '')
186
+
187
+ if not original_prompt or not user_response: return jsonify({"error": "Original prompt and user response are required."}), 400
188
+ if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client):
189
+ return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503
190
+
191
+ system_instruction = (
192
+ "You are an expert English teacher providing feedback. "
193
+ f"The user's study focus is '{context_focus}'. "
194
+ "Your entire response MUST be in English. "
195
+ "Provide clear, constructive feedback on the user's writing. "
196
+ "Point out grammar, spelling, or style errors. "
197
+ "Offer a corrected or improved version of their text. "
198
+ "Structure your feedback with markdown for clarity (e.g., using ### Corrected Version)."
199
+ )
200
+ user_prompt = f"The original task was: \"{original_prompt}\"\n\nHere is the user's response:\n---\n{user_response}\n---\nPlease provide your feedback."
201
+ try:
202
+ feedback_text = get_ai_text_response(model_provider, model_name, system_instruction, user_prompt)
203
+ return jsonify({"feedback": feedback_text})
204
+ except Exception as e:
205
+ return jsonify({"error": f"AI feedback failed: {e}"}), 500
206
+
207
+ @app.route('/analyze-image', methods=['POST'])
208
+ def analyze_image():
209
+ if not genai_client: return jsonify({"error": "GEMINI_API_KEY not configured."}), 503
210
+ data = request.get_json()
211
+ base64_image = data.get('image')
212
+ model_value = data.get('model', 'gemini:gemini-2.5-flash-latest')
213
+
214
+ model_name = 'gemini-2.5-flash-latest' # Default
215
+ if model_value.startswith('gemini:'):
216
+ model_name = model_value.split(':', 1)[1]
217
+
218
+ if not base64_image: return jsonify({"error": "No image data."}), 400
219
+ try:
220
+ image = Image.open(io.BytesIO(base64.b64decode(base64_image.split(',')[1])))
221
+ model = genai_client.GenerativeModel(model_name)
222
+ schema = { "type": "object", "properties": { "vocabulary": { "type": "array", "items": { "type": "object", "properties": { "term": {"type": "string"}, "definition": {"type": "string"} }, "required": ["term", "definition"] } } }, "required": ["vocabulary"] }
223
+ prompt = [ "Act as an English teacher. Identify 5-7 key objects/concepts in this image. For each, provide its English name and a simple definition. Return a single JSON object conforming to the schema.", image ]
224
+
225
+ config = GenerationConfig(response_mime_type="application/json", response_schema=schema)
226
+ response = model.generate_content(prompt, generation_config=config)
227
+
228
+ return jsonify(json.loads(response.text)['vocabulary'])
229
+ except Exception as e:
230
+ return jsonify({"error": f"Image analysis failed: {e}"}), 500
231
+
232
+ @app.route('/chat-with-ai', methods=['POST'])
233
+ def chat_with_ai():
234
+ if not groq_client: return jsonify({"error": "GROQ_API_KEY not configured."}), 503
235
+ data = request.get_json()
236
+ history, user_message = data.get('history', []), data.get('message', '')
237
+ if not user_message: return jsonify({"error": "No message."}), 400
238
+ try:
239
+ system = "You are 'Groq Chat', a friendly English tutor. Keep responses concise (1-2 sentences). If the user makes a grammar mistake, gently correct it. Ask questions to keep the conversation flowing. Always respond in English."
240
+ messages = [{"role": "system", "content": system}] + history + [{"role": "user", "content": user_message}]
241
+ response = groq_client.chat.completions.create(model="llama-3.1-8b-instant", messages=messages, temperature=0.7)
242
+
243
+ # Track token usage
244
+ user = get_current_user()
245
+ if user and hasattr(response, 'usage'):
246
+ track_token_usage(
247
+ user['id'],
248
+ 'groq',
249
+ response.usage.prompt_tokens,
250
+ response.usage.completion_tokens,
251
+ 'conversation'
252
+ )
253
+
254
+ return jsonify({"response": response.choices[0].message.content.strip()})
255
+ except Exception as e:
256
+ return jsonify({"error": f"AI chat failed: {e}"}), 500
257
+
258
+ @app.route('/pronunciation-feedback', methods=['POST'])
259
+ def pronunciation_feedback():
260
+ if not groq_client: return jsonify({"error": "GROQ_API_KEY not configured."}), 503
261
+ data = request.get_json()
262
+ target_text, user_text = data.get('target_text'), data.get('user_text')
263
+ if not target_text or not user_text: return jsonify({"error": "Required data missing."}), 400
264
+ try:
265
+ system_instruction = "You are an expert American English pronunciation coach. The user tried to say a target sentence, and their speech was transcribed. Based on the likely pronunciation differences, provide brief, friendly, and actionable feedback in Portuguese. Focus on 1-2 key points. If it's very close, praise the user."
266
+ user_prompt = f"Target: \"{target_text}\"\nTranscription: \"{user_text}\"\n\nProvide pronunciation feedback."
267
+ messages = [{"role": "system", "content": system_instruction}, {"role": "user", "content": user_prompt}]
268
+ response = groq_client.chat.completions.create(model="llama-3.1-8b-instant", messages=messages, temperature=0.5)
269
+
270
+ # Track token usage
271
+ user = get_current_user()
272
+ if user and hasattr(response, 'usage'):
273
+ track_token_usage(
274
+ user['id'],
275
+ 'groq',
276
+ response.usage.prompt_tokens,
277
+ response.usage.completion_tokens,
278
+ 'pronunciation_feedback'
279
+ )
280
+
281
+ return jsonify({"feedback": response.choices[0].message.content.strip()})
282
+ except Exception as e:
283
+ return jsonify({"error": f"Pronunciation analysis failed: {e}"}), 500
284
+
285
+ @app.route('/generate-image', methods=['POST'])
286
+ def generate_image():
287
+ if not genai_client: return jsonify({"error": "GEMINI_API_KEY not configured."}), 503
288
+ data = request.get_json()
289
+ prompt = data.get('prompt')
290
+ if not prompt: return jsonify({"error": "Image prompt is required."}), 400
291
+ try:
292
+ model = genai_client.GenerativeModel(model_name='gemini-2.5-flash-image-preview')
293
+ response = model.generate_content(prompt)
294
+ base64_image_data = response.parts[0].inline_data.data
295
+ return jsonify({"image_base64": base64_image_data})
296
+ except Exception as e:
297
+ return jsonify({"error": f"Image generation failed: {e}"}), 500
298
+
299
+ # --- FUNÇÃO AUXILIAR E ROTA RAIZ ---
300
+ def get_ai_text_response(provider, model_name, system_instruction, user_prompt, json_schema=None):
301
+ if provider == 'gemini':
302
+ model = genai_client.GenerativeModel(model_name, system_instruction=system_instruction)
303
+
304
+ config = None
305
+ if json_schema:
306
+ config = GenerationConfig(response_mime_type="application/json", response_schema=json_schema)
307
+
308
+ response = model.generate_content(user_prompt, generation_config=config)
309
+
310
+ if json_schema:
311
+ parsed_json = json.loads(response.text)
312
+ required_keys = json_schema.get("required", [])
313
+ if not all(key in parsed_json and parsed_json[key] for key in required_keys):
314
+ raise ValueError(f"AI response missing required keys or has empty values.")
315
+ return parsed_json
316
+ else:
317
+ return response.text.strip()
318
+
319
+ elif provider == 'groq':
320
+ final_user_prompt = user_prompt
321
+ if json_schema:
322
+ final_user_prompt += f"\n\nYou MUST respond with a single JSON object that strictly follows this schema. Do not add any other text before or after the JSON object:\n{json.dumps(json_schema)}"
323
+
324
+ messages = [{"role": "system", "content": system_instruction}, {"role": "user", "content": final_user_prompt}]
325
+ config = {'response_format': {"type": "json_object"}} if json_schema else {}
326
+ response = groq_client.chat.completions.create(model=model_name, messages=messages, **config)
327
+
328
+ if json_schema:
329
+ parsed_json = json.loads(response.choices[0].message.content)
330
+ required_keys = json_schema.get("required", [])
331
+ if not all(key in parsed_json and parsed_json[key] for key in required_keys):
332
+ raise ValueError(f"AI response missing required keys or has empty values.")
333
+ return parsed_json
334
+ else:
335
+ return response.choices[0].message.content.strip()
336
+
337
+ raise Exception(f"Unsupported provider: {provider}")
338
+
339
+ # --- AUTHENTICATION ROUTES ---
340
+
341
+ @app.route('/register', methods=['POST'])
342
+ def register():
343
+ """User registration endpoint"""
344
+ try:
345
+ data = request.get_json()
346
+ email = data.get('email', '').strip().lower()
347
+ password = data.get('password', '')
348
+
349
+ # Validate input
350
+ if not email or not password:
351
+ return jsonify({'error': 'Email and password are required'}), 400
352
+
353
+ if len(password) < 8:
354
+ return jsonify({'error': 'Password must be at least 8 characters long'}), 400
355
+
356
+ # Validate email format
357
+ try:
358
+ validate_email(email)
359
+ except EmailNotValidError:
360
+ return jsonify({'error': 'Invalid email format'}), 400
361
+
362
+ # Create user
363
+ result = create_user(email, password)
364
+
365
+ if result['success']:
366
+ # Send confirmation email
367
+ if send_confirmation_email(email, result['confirmation_token']):
368
+ return jsonify({
369
+ 'message': 'Registration successful! Please check your email to confirm your account.',
370
+ 'email_sent': True
371
+ }), 201
372
+ else:
373
+ return jsonify({
374
+ 'message': 'Registration successful! However, we could not send the confirmation email. Please contact support.',
375
+ 'email_sent': False
376
+ }), 201
377
+ else:
378
+ return jsonify({'error': result['message']}), 400
379
+
380
+ except Exception as e:
381
+ print(f"Registration error: {e}")
382
+ return jsonify({'error': 'Internal server error'}), 500
383
+
384
+ @app.route('/login', methods=['POST'])
385
+ def login():
386
+ """User login endpoint"""
387
+ try:
388
+ data = request.get_json()
389
+ email = data.get('email', '').strip().lower()
390
+ password = data.get('password', '')
391
+
392
+ if not email or not password:
393
+ return jsonify({'error': 'Email and password are required'}), 400
394
+
395
+ result = authenticate_user(email, password)
396
+
397
+ if result['success']:
398
+ session['user_id'] = result['user_id']
399
+ session['user_email'] = result['email']
400
+ session.permanent = True
401
+
402
+ # Get user settings
403
+ settings = get_user_settings(result['user_id'])
404
+
405
+ return jsonify({
406
+ 'message': 'Login successful',
407
+ 'user': {
408
+ 'id': result['user_id'],
409
+ 'email': result['email'],
410
+ 'settings': settings
411
+ }
412
+ }), 200
413
+ else:
414
+ return jsonify({'error': result['message']}), 401
415
+
416
+ except Exception as e:
417
+ print(f"Login error: {e}")
418
+ return jsonify({'error': 'Internal server error'}), 500
419
+
420
+ @app.route('/logout', methods=['POST'])
421
+ def logout():
422
+ """User logout endpoint"""
423
+ session.clear()
424
+ return jsonify({'message': 'Logout successful'}), 200
425
+
426
+ @app.route('/confirm-email')
427
+ def confirm_email_route():
428
+ """Email confirmation endpoint"""
429
+ token = request.args.get('token')
430
+
431
+ if not token:
432
+ return render_template_string('''
433
+ <!DOCTYPE html>
434
+ <html><head><title>Invalid Link</title></head>
435
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
436
+ <h2>Invalid Confirmation Link</h2>
437
+ <p>This confirmation link is invalid or malformed.</p>
438
+ <a href="/" style="color: #4f46e5;">Return to English Helper</a>
439
+ </body></html>
440
+ '''), 400
441
+
442
+ result = confirm_email(token)
443
+
444
+ if result['success']:
445
+ return render_template_string('''
446
+ <!DOCTYPE html>
447
+ <html><head><title>Email Confirmed</title></head>
448
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
449
+ <h2>βœ… Email Confirmed!</h2>
450
+ <p>Your email has been successfully confirmed. You can now log in to your account.</p>
451
+ <a href="/" style="background: #4f46e5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Continue to English Helper</a>
452
+ </body></html>
453
+ ''')
454
+ else:
455
+ return render_template_string('''
456
+ <!DOCTYPE html>
457
+ <html><head><title>Confirmation Failed</title></head>
458
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
459
+ <h2>❌ Confirmation Failed</h2>
460
+ <p>This confirmation link is invalid or has expired.</p>
461
+ <a href="/" style="color: #4f46e5;">Return to English Helper</a>
462
+ </body></html>
463
+ '''), 400
464
+
465
+ @app.route('/user/profile', methods=['GET'])
466
+ @login_required
467
+ def get_user_profile():
468
+ """Get current user profile"""
469
+ user = get_current_user()
470
+ if not user:
471
+ return jsonify({'error': 'User not found'}), 404
472
+
473
+ settings = get_user_settings(user['id'])
474
+ flashcards_count = len(get_user_flashcards(user['id'], 1000))
475
+
476
+ return jsonify({
477
+ 'user': {
478
+ 'id': user['id'],
479
+ 'email': user['email'],
480
+ 'settings': settings,
481
+ 'stats': {
482
+ 'flashcards_created': flashcards_count
483
+ }
484
+ }
485
+ })
486
+
487
+ @app.route('/user/settings', methods=['GET', 'POST'])
488
+ @login_required
489
+ def user_settings():
490
+ """Get or update user settings"""
491
+ user = get_current_user()
492
+ if not user:
493
+ return jsonify({'error': 'User not found'}), 404
494
+
495
+ if request.method == 'GET':
496
+ settings = get_user_settings(user['id'])
497
+ return jsonify({'settings': settings})
498
+
499
+ elif request.method == 'POST':
500
+ data = request.get_json()
501
+ settings = {
502
+ 'preferred_model': data.get('preferred_model'),
503
+ 'context_focus': data.get('context_focus'),
504
+ 'voice_accent': data.get('voice_accent'),
505
+ 'daily_goal': data.get('daily_goal', 10),
506
+ 'notification_enabled': data.get('notification_enabled', True)
507
+ }
508
+
509
+ if update_user_settings(user['id'], settings):
510
+ return jsonify({'message': 'Settings updated successfully'})
511
+ else:
512
+ return jsonify({'error': 'Failed to update settings'}), 500
513
+
514
+ @app.route('/user/flashcards', methods=['GET', 'POST'])
515
+ @login_required
516
+ def user_flashcards():
517
+ """Get user flashcards or save new flashcard"""
518
+ user = get_current_user()
519
+ if not user:
520
+ return jsonify({'error': 'User not found'}), 404
521
+
522
+ if request.method == 'GET':
523
+ flashcards = get_user_flashcards(user['id'])
524
+ return jsonify({'flashcards': flashcards})
525
+
526
+ elif request.method == 'POST':
527
+ data = request.get_json()
528
+ if save_user_flashcard(user['id'], data):
529
+ return jsonify({'message': 'Flashcard saved successfully'})
530
+ else:
531
+ return jsonify({'error': 'Failed to save flashcard'}), 500
532
+
533
+ @app.route('/auth/check', methods=['GET'])
534
+ def check_auth():
535
+ """Check if user is authenticated"""
536
+ user = get_current_user()
537
+ if user:
538
+ settings = get_user_settings(user['id'])
539
+ return jsonify({
540
+ 'authenticated': True,
541
+ 'user': {
542
+ 'id': user['id'],
543
+ 'email': user['email'],
544
+ 'settings': settings
545
+ }
546
+ })
547
+ else:
548
+ return jsonify({'authenticated': False})
549
+
550
+ # --- MODIFIED EXISTING ROUTES TO SUPPORT USER DATA ---
551
+
552
+ # Override the original explain-proxy to save flashcards for logged-in users
553
+ @app.route('/explain-proxy', methods=['POST'])
554
+ def explain_proxy():
555
+ data = request.get_json()
556
+ model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1)
557
+ context_focus = data.get('context_focus', 'General/Social')
558
+ custom_prompt = data.get('custom_prompt', None)
559
+ word = data.get('word', '').strip()
560
+ context = data.get('context', '')
561
+ for_flashcard = data.get('for_flashcard', False)
562
+
563
+ if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client):
564
+ return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503
565
+
566
+ system_instruction_base = f"You are a professional English tutor. The user's study focus is '{context_focus}'. All your responses must be in ENGLISH."
567
+ try:
568
+ if custom_prompt:
569
+ activity_text = get_ai_text_response(model_provider, model_name, system_instruction_base, custom_prompt)
570
+ return jsonify({"explanation": activity_text})
571
+
572
+ if not word: return jsonify({"error": "No word selected."}), 400
573
+
574
+ if for_flashcard:
575
+ schema = {"type": "object", "properties": {"term": {"type": "string"}, "translation": {"type": "string"}, "context_sentence": {"type": "string"}, "gapped_sentence": {"type": "string"}, "definition": {"type": "string"}}, "required": ["term", "translation", "context_sentence", "gapped_sentence", "definition"]}
576
+ prompt = f"Analyze '{word}' in context: '{context}'. Generate a JSON for a flashcard. The 'gapped_sentence' must replace '{word}' with '______________'."
577
+ flashcard_data = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt, json_schema=schema)
578
+
579
+ # Save flashcard for logged-in users
580
+ user = get_current_user()
581
+ if user:
582
+ save_user_flashcard(user['id'], flashcard_data)
583
+
584
+ return jsonify(flashcard_data)
585
+ else:
586
+ prompt = f"Analyze '{word}' in context: '{context}'. Provide a one-sentence English explanation, then '---', then the Portuguese translation."
587
+ parts = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt).split('---', 1)
588
+ return jsonify({"explanation": parts[0].strip(), "translation": parts[1].strip() if len(parts) > 1 else 'N/A'})
589
+ except Exception as e:
590
+ print(f"AI ANALYSIS ERROR in /explain-proxy: {e}")
591
+ return jsonify({"error": f"AI analysis failed: {e}"}), 500
592
+
593
+ # --- CONTENT CURATION ROUTES ---
594
+
595
+ @app.route('/content/search', methods=['POST'])
596
+ @login_required
597
+ def search_content():
598
+ """Search for content based on user interests"""
599
+ try:
600
+ user = get_current_user()
601
+ if not user:
602
+ return jsonify({'error': 'User not found'}), 404
603
+
604
+ data = request.get_json()
605
+ query = data.get('query', '')
606
+ category = data.get('category', '')
607
+
608
+ # Get user settings and interests
609
+ settings = get_user_settings(user['id'])
610
+ interests = get_user_interests(user['id'])
611
+
612
+ if not interests and query:
613
+ # Use query as interest if no interests set
614
+ interests = {query: 1.0}
615
+
616
+ english_level = settings.get('english_level', 'B1') if settings else 'B1'
617
+ context_focus = settings.get('context_focus', 'General/Social') if settings else 'General/Social'
618
+
619
+ # Search for content
620
+ results = content_curator.search_content(
621
+ interests=list(interests.keys()) if interests else [query],
622
+ english_level=english_level,
623
+ context_focus=context_focus,
624
+ limit=10
625
+ )
626
+
627
+ return jsonify({'results': results})
628
+
629
+ except Exception as e:
630
+ print(f"Content search error: {e}")
631
+ return jsonify({'error': 'Content search failed'}), 500
632
+
633
+ @app.route('/content/extract', methods=['POST'])
634
+ @login_required
635
+ def extract_content():
636
+ """Extract content from URL"""
637
+ try:
638
+ data = request.get_json()
639
+ url = data.get('url', '')
640
+
641
+ if not url:
642
+ return jsonify({'error': 'URL required'}), 400
643
+
644
+ result = content_curator.extract_content_from_url(url)
645
+ return jsonify(result)
646
+
647
+ except Exception as e:
648
+ print(f"Content extraction error: {e}")
649
+ return jsonify({'error': 'Content extraction failed'}), 500
650
+
651
+ @app.route('/content/save', methods=['POST'])
652
+ @login_required
653
+ def save_content():
654
+ """Save content/article for user"""
655
+ try:
656
+ user = get_current_user()
657
+ if not user:
658
+ return jsonify({'error': 'User not found'}), 404
659
+
660
+ data = request.get_json()
661
+ title = data.get('title', '')
662
+ content = data.get('content', '')
663
+ source_url = data.get('source_url')
664
+ source_type = data.get('source_type', 'manual')
665
+ category = data.get('category')
666
+
667
+ if not title or not content:
668
+ return jsonify({'error': 'Title and content required'}), 400
669
+
670
+ result = save_user_article(user['id'], title, content, source_url, source_type, category)
671
+
672
+ if result['success']:
673
+ # Record analytics
674
+ record_analytics_metric(user['id'], 'content_saved', 1)
675
+ return jsonify({'message': 'Content saved successfully', 'article_id': result['article_id']})
676
+ else:
677
+ return jsonify({'error': result['message']}), 500
678
+
679
+ except Exception as e:
680
+ print(f"Save content error: {e}")
681
+ return jsonify({'error': 'Failed to save content'}), 500
682
+
683
+ @app.route('/content/articles', methods=['GET'])
684
+ @login_required
685
+ def get_articles():
686
+ """Get user's saved articles"""
687
+ try:
688
+ user = get_current_user()
689
+ if not user:
690
+ return jsonify({'error': 'User not found'}), 404
691
+
692
+ category = request.args.get('category')
693
+ limit = int(request.args.get('limit', 50))
694
+
695
+ articles = get_user_articles(user['id'], category, limit)
696
+ return jsonify({'articles': articles})
697
+
698
+ except Exception as e:
699
+ print(f"Get articles error: {e}")
700
+ return jsonify({'error': 'Failed to get articles'}), 500
701
+
702
+ @app.route('/content/interests', methods=['GET', 'POST'])
703
+ @login_required
704
+ def manage_interests():
705
+ """Get or update user interests"""
706
+ try:
707
+ user = get_current_user()
708
+ if not user:
709
+ return jsonify({'error': 'User not found'}), 404
710
+
711
+ if request.method == 'GET':
712
+ interests = get_user_interests(user['id'])
713
+ return jsonify({'interests': interests})
714
+
715
+ elif request.method == 'POST':
716
+ data = request.get_json()
717
+ interests = data.get('interests', {})
718
+
719
+ if update_user_interests(user['id'], interests):
720
+ return jsonify({'message': 'Interests updated successfully'})
721
+ else:
722
+ return jsonify({'error': 'Failed to update interests'}), 500
723
+
724
+ except Exception as e:
725
+ print(f"Manage interests error: {e}")
726
+ return jsonify({'error': 'Failed to manage interests'}), 500
727
+
728
+ @app.route('/content/recommendations', methods=['GET'])
729
+ @login_required
730
+ def get_recommendations():
731
+ """Get AI-powered content recommendations"""
732
+ try:
733
+ user = get_current_user()
734
+ if not user:
735
+ return jsonify({'error': 'User not found'}), 404
736
+
737
+ # Get user data
738
+ interests = get_user_interests(user['id'])
739
+ recent_articles = get_user_articles(user['id'], limit=10)
740
+ settings = get_user_settings(user['id'])
741
+
742
+ english_level = settings.get('english_level', 'B1') if settings else 'B1'
743
+ context_focus = settings.get('context_focus', 'General/Social') if settings else 'General/Social'
744
+
745
+ # Generate recommendations
746
+ recommendations = content_curator.generate_personalized_recommendations(
747
+ interests, recent_articles, english_level, context_focus, user['id']
748
+ )
749
+
750
+ return jsonify({'recommendations': recommendations})
751
+
752
+ except Exception as e:
753
+ print(f"Recommendations error: {e}")
754
+ return jsonify({'error': 'Failed to get recommendations'}), 500
755
+
756
+ @app.route('/content/analyze', methods=['POST'])
757
+ @login_required
758
+ def analyze_content():
759
+ """Analyze content for learning insights"""
760
+ try:
761
+ user = get_current_user()
762
+ if not user:
763
+ return jsonify({'error': 'User not found'}), 404
764
+
765
+ data = request.get_json()
766
+ content = data.get('content', '')
767
+
768
+ if not content:
769
+ return jsonify({'error': 'Content required'}), 400
770
+
771
+ settings = get_user_settings(user['id'])
772
+ english_level = settings.get('english_level', 'B1') if settings else 'B1'
773
+
774
+ analysis = content_curator.analyze_content_for_learning(content, english_level)
775
+
776
+ return jsonify({'analysis': analysis})
777
+
778
+ except Exception as e:
779
+ print(f"Content analysis error: {e}")
780
+ return jsonify({'error': 'Content analysis failed'}), 500
781
+
782
+ # --- STUDY PLANNING ROUTES ---
783
+
784
+ @app.route('/study/plans', methods=['GET', 'POST'])
785
+ @login_required
786
+ def manage_study_plans():
787
+ """Get or create study plans"""
788
+ try:
789
+ user = get_current_user()
790
+ if not user:
791
+ return jsonify({'error': 'User not found'}), 404
792
+
793
+ if request.method == 'GET':
794
+ plans = get_user_study_plans(user['id'])
795
+ return jsonify({'plans': plans})
796
+
797
+ elif request.method == 'POST':
798
+ data = request.get_json()
799
+ plan_name = data.get('plan_name', '')
800
+ target_level = data.get('target_level', 'B2')
801
+ current_level = data.get('current_level', 'B1')
802
+ objectives = json.dumps(data.get('objectives', []))
803
+ weekly_hours = data.get('weekly_hours', 5)
804
+
805
+ if not plan_name:
806
+ return jsonify({'error': 'Plan name required'}), 400
807
+
808
+ result = create_study_plan(user['id'], plan_name, target_level, current_level, objectives, weekly_hours)
809
+
810
+ if result['success']:
811
+ return jsonify({'message': 'Study plan created', 'plan_id': result['plan_id']})
812
+ else:
813
+ return jsonify({'error': result['message']}), 500
814
+
815
+ except Exception as e:
816
+ print(f"Study plans error: {e}")
817
+ return jsonify({'error': 'Failed to manage study plans'}), 500
818
+
819
+ @app.route('/analytics/dashboard', methods=['GET'])
820
+ @login_required
821
+ def analytics_dashboard():
822
+ """Get analytics dashboard data"""
823
+ try:
824
+ user = get_current_user()
825
+ if not user:
826
+ return jsonify({'error': 'User not found'}), 404
827
+
828
+ days = int(request.args.get('days', 30))
829
+
830
+ # Get various analytics
831
+ analytics_data = {
832
+ 'flashcards_created': get_user_analytics(user['id'], 'flashcards_created', days),
833
+ 'content_saved': get_user_analytics(user['id'], 'content_saved', days),
834
+ 'study_sessions': get_user_analytics(user['id'], 'study_session', days),
835
+ 'total_flashcards': len(get_user_flashcards(user['id'], 1000)),
836
+ 'total_articles': len(get_user_articles(user['id'], limit=1000)),
837
+ 'user_level': get_user_settings(user['id']).get('english_level', 'B1')
838
+ }
839
+
840
+ return jsonify({'analytics': analytics_data})
841
+
842
+ except Exception as e:
843
+ print(f"Analytics error: {e}")
844
+ return jsonify({'error': 'Failed to get analytics'}), 500
845
+
846
+ # --- STUDY PLANNER ROUTES ---
847
+
848
+ @app.route('/study-plan/create', methods=['POST'])
849
+ @login_required
850
+ def create_study_plan_route():
851
+ """Create a personalized study plan"""
852
+ try:
853
+ user = get_current_user()
854
+ if not user:
855
+ return jsonify({'error': 'User not found'}), 404
856
+
857
+ data = request.get_json()
858
+
859
+ # Get user settings and interests
860
+ user_settings = get_user_settings(user['id'])
861
+ user_interests = get_user_interests(user['id'])
862
+
863
+ # Prepare data for study planner
864
+ planner_data = {
865
+ 'english_level': data.get('current_level') or user_settings.get('english_level', 'B1'),
866
+ 'target_level': data.get('target_level', 'B2'),
867
+ 'weekly_hours': int(data.get('weekly_hours', 5)),
868
+ 'context_focus': data.get('context_focus') or user_settings.get('context_focus', 'General/Social'),
869
+ 'interests': user_interests,
870
+ 'study_goals': data.get('study_goals', [])
871
+ }
872
+
873
+ # Generate the plan
874
+ result = study_planner.generate_personalized_plan(planner_data)
875
+
876
+ if result['success']:
877
+ plan = result['plan']
878
+
879
+ # Save to database
880
+ plan_id = create_study_plan(
881
+ user['id'],
882
+ plan['target_level'],
883
+ plan['weekly_hours'],
884
+ plan['estimated_weeks'],
885
+ json.dumps(plan)
886
+ )
887
+
888
+ plan['id'] = plan_id
889
+
890
+ # Record analytics
891
+ record_analytics_metric(user['id'], 'study_plan_created', 1)
892
+
893
+ return jsonify({'success': True, 'plan': plan})
894
+ else:
895
+ return jsonify({'success': False, 'error': result['error']}), 500
896
+
897
+ except Exception as e:
898
+ print(f"Study plan creation error: {e}")
899
+ return jsonify({'error': 'Failed to create study plan'}), 500
900
+
901
+ @app.route('/study-plan/current', methods=['GET'])
902
+ @login_required
903
+ def get_current_study_plan():
904
+ """Get user's current study plan"""
905
+ try:
906
+ user = get_current_user()
907
+ if not user:
908
+ return jsonify({'error': 'User not found'}), 404
909
+
910
+ plans = get_user_study_plans(user['id'])
911
+
912
+ if plans:
913
+ # Get the most recent active plan
914
+ current_plan = plans[0] # Assuming most recent first
915
+
916
+ # Parse the plan data
917
+ plan_data = json.loads(current_plan['plan_data'])
918
+
919
+ # Add database ID
920
+ plan_data['db_id'] = current_plan['id']
921
+
922
+ # Get activities for this plan
923
+ activities = get_study_activities(current_plan['id'])
924
+ plan_data['completed_activities'] = activities
925
+
926
+ return jsonify({'success': True, 'plan': plan_data})
927
+ else:
928
+ return jsonify({'success': True, 'plan': None})
929
+
930
+ except Exception as e:
931
+ print(f"Get study plan error: {e}")
932
+ return jsonify({'error': 'Failed to get study plan'}), 500
933
+
934
+ @app.route('/study-plan/activity/complete', methods=['POST'])
935
+ @login_required
936
+ def complete_study_activity():
937
+ """Mark a study activity as completed"""
938
+ try:
939
+ user = get_current_user()
940
+ if not user:
941
+ return jsonify({'error': 'User not found'}), 404
942
+
943
+ data = request.get_json()
944
+ plan_id = data.get('plan_id')
945
+ activity_id = data.get('activity_id')
946
+ duration_minutes = data.get('duration_minutes', 0)
947
+ notes = data.get('notes', '')
948
+
949
+ if not plan_id or not activity_id:
950
+ return jsonify({'error': 'Missing plan_id or activity_id'}), 400
951
+
952
+ # Add activity completion
953
+ add_study_activity(plan_id, activity_id, duration_minutes, notes)
954
+
955
+ # Record analytics
956
+ record_analytics_metric(user['id'], 'study_activity_completed', 1)
957
+ record_analytics_metric(user['id'], 'study_time_minutes', duration_minutes)
958
+
959
+ return jsonify({'success': True})
960
+
961
+ except Exception as e:
962
+ print(f"Complete activity error: {e}")
963
+ return jsonify({'error': 'Failed to complete activity'}), 500
964
+
965
+ @app.route('/study-plan/progress', methods=['GET'])
966
+ @login_required
967
+ def get_study_progress():
968
+ """Get study plan progress analytics"""
969
+ try:
970
+ user = get_current_user()
971
+ if not user:
972
+ return jsonify({'error': 'User not found'}), 404
973
+
974
+ plans = get_user_study_plans(user['id'])
975
+
976
+ if not plans:
977
+ return jsonify({'success': True, 'progress': None})
978
+
979
+ current_plan = plans[0]
980
+ plan_data = json.loads(current_plan['plan_data'])
981
+ activities = get_study_activities(current_plan['id'])
982
+
983
+ # Calculate progress
984
+ total_activities = len(plan_data.get('activities', []))
985
+ completed_activities = len(activities)
986
+
987
+ progress_data = {
988
+ 'total_activities': total_activities,
989
+ 'completed_activities': completed_activities,
990
+ 'completion_percentage': (completed_activities / max(total_activities, 1)) * 100,
991
+ 'estimated_weeks': plan_data.get('estimated_weeks', 0),
992
+ 'weeks_elapsed': max(1, (datetime.now() - datetime.fromisoformat(current_plan['created_at'])).days // 7),
993
+ 'target_level': plan_data.get('target_level', 'B2'),
994
+ 'weekly_hours': plan_data.get('weekly_hours', 5),
995
+ 'recent_activities': activities[-10:] if activities else [] # Last 10 activities
996
+ }
997
+
998
+ return jsonify({'success': True, 'progress': progress_data})
999
+
1000
+ except Exception as e:
1001
+ print(f"Study progress error: {e}")
1002
+ return jsonify({'error': 'Failed to get study progress'}), 500
1003
+
1004
+ # --- ADMIN ROUTES ---
1005
+
1006
+ @app.route('/admin/login', methods=['POST'])
1007
+ def admin_login():
1008
+ """Admin login endpoint"""
1009
+ try:
1010
+ data = request.get_json()
1011
+ username = data.get('username')
1012
+ password = data.get('password')
1013
+
1014
+ if admin_manager.login_admin(username, password):
1015
+ return jsonify({'success': True, 'message': 'Admin logged in successfully'})
1016
+ else:
1017
+ return jsonify({'success': False, 'error': 'Invalid credentials'}), 401
1018
+
1019
+ except Exception as e:
1020
+ print(f"Admin login error: {e}")
1021
+ return jsonify({'error': 'Admin login failed'}), 500
1022
+
1023
+ @app.route('/admin/logout', methods=['POST'])
1024
+ @admin_required
1025
+ def admin_logout():
1026
+ """Admin logout endpoint"""
1027
+ try:
1028
+ admin_manager.logout_admin()
1029
+ return jsonify({'success': True, 'message': 'Admin logged out successfully'})
1030
+ except Exception as e:
1031
+ print(f"Admin logout error: {e}")
1032
+ return jsonify({'error': 'Admin logout failed'}), 500
1033
+
1034
+ @app.route('/admin/check', methods=['GET'])
1035
+ def admin_check():
1036
+ """Check admin authentication status"""
1037
+ try:
1038
+ is_authenticated = admin_manager.is_admin_logged_in()
1039
+ return jsonify({
1040
+ 'authenticated': is_authenticated,
1041
+ 'username': session.get('admin_username') if is_authenticated else None
1042
+ })
1043
+ except Exception as e:
1044
+ print(f"Admin check error: {e}")
1045
+ return jsonify({'authenticated': False})
1046
+
1047
+ @app.route('/admin/dashboard', methods=['GET'])
1048
+ @admin_required
1049
+ def admin_dashboard():
1050
+ """Get admin dashboard data"""
1051
+ try:
1052
+ stats = admin_manager.get_system_stats()
1053
+ return jsonify({'success': True, 'stats': stats})
1054
+ except Exception as e:
1055
+ print(f"Admin dashboard error: {e}")
1056
+ return jsonify({'error': 'Failed to load dashboard'}), 500
1057
+
1058
+ @app.route('/admin/users', methods=['GET'])
1059
+ @admin_required
1060
+ def admin_get_users():
1061
+ """Get paginated list of users"""
1062
+ try:
1063
+ page = int(request.args.get('page', 1))
1064
+ per_page = int(request.args.get('per_page', 20))
1065
+
1066
+ users_data = admin_manager.get_all_users(page, per_page)
1067
+ return jsonify({'success': True, 'data': users_data})
1068
+ except Exception as e:
1069
+ print(f"Admin get users error: {e}")
1070
+ return jsonify({'error': 'Failed to get users'}), 500
1071
+
1072
+ @app.route('/admin/users/<int:user_id>', methods=['GET'])
1073
+ @admin_required
1074
+ def admin_get_user_details(user_id):
1075
+ """Get detailed information about a user"""
1076
+ try:
1077
+ user_details = admin_manager.get_user_details(user_id)
1078
+ if user_details:
1079
+ return jsonify({'success': True, 'user': user_details})
1080
+ else:
1081
+ return jsonify({'error': 'User not found'}), 404
1082
+ except Exception as e:
1083
+ print(f"Admin get user details error: {e}")
1084
+ return jsonify({'error': 'Failed to get user details'}), 500
1085
+
1086
+ @app.route('/admin/users/<int:user_id>', methods=['DELETE'])
1087
+ @admin_required
1088
+ def admin_delete_user(user_id):
1089
+ """Delete a user and all associated data"""
1090
+ try:
1091
+ if admin_manager.delete_user(user_id):
1092
+ return jsonify({'success': True, 'message': 'User deleted successfully'})
1093
+ else:
1094
+ return jsonify({'error': 'Failed to delete user'}), 500
1095
+ except Exception as e:
1096
+ print(f"Admin delete user error: {e}")
1097
+ return jsonify({'error': 'Failed to delete user'}), 500
1098
+
1099
+ @app.route('/admin/database/schema', methods=['GET'])
1100
+ @admin_required
1101
+ def admin_get_database_schema():
1102
+ """Get database schema information"""
1103
+ try:
1104
+ schema = admin_manager.get_database_schema()
1105
+ return jsonify({'success': True, 'schema': schema})
1106
+ except Exception as e:
1107
+ print(f"Admin get schema error: {e}")
1108
+ return jsonify({'error': 'Failed to get database schema'}), 500
1109
+
1110
+ @app.route('/admin/token-usage', methods=['POST'])
1111
+ def record_token_usage():
1112
+ """Record token usage (called by AI functions)"""
1113
+ try:
1114
+ data = request.get_json()
1115
+ user_id = data.get('user_id')
1116
+ api_provider = data.get('api_provider')
1117
+ input_tokens = data.get('input_tokens', 0)
1118
+ output_tokens = data.get('output_tokens', 0)
1119
+ operation_type = data.get('operation_type', 'unknown')
1120
+
1121
+ admin_manager.record_token_usage(
1122
+ user_id, api_provider, input_tokens, output_tokens, operation_type
1123
+ )
1124
+
1125
+ return jsonify({'success': True})
1126
+ except Exception as e:
1127
+ print(f"Token usage recording error: {e}")
1128
+ return jsonify({'error': 'Failed to record token usage'}), 500
1129
+
1130
+ @app.route('/admin/export/users', methods=['GET'])
1131
+ @admin_required
1132
+ def export_users():
1133
+ """Export users data as CSV"""
1134
+ try:
1135
+ import csv
1136
+ from io import StringIO
1137
+
1138
+ users_data = admin_manager.get_all_users(page=1, per_page=10000) # Get all users
1139
+
1140
+ output = StringIO()
1141
+ writer = csv.writer(output)
1142
+
1143
+ # Write header
1144
+ writer.writerow(['ID', 'Email', 'Created At', 'Email Confirmed', 'Last Login', 'Sessions', 'Flashcards', 'Articles'])
1145
+
1146
+ # Write data
1147
+ for user in users_data['users']:
1148
+ writer.writerow([
1149
+ user['id'],
1150
+ user['email'],
1151
+ user['created_at'],
1152
+ user['email_confirmed'],
1153
+ user['last_login'] or 'Never',
1154
+ user['session_count'],
1155
+ user['flashcard_count'],
1156
+ user['article_count']
1157
+ ])
1158
+
1159
+ output.seek(0)
1160
+
1161
+ return Response(
1162
+ output.getvalue(),
1163
+ mimetype='text/csv',
1164
+ headers={'Content-Disposition': f'attachment; filename=users_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'}
1165
+ )
1166
+
1167
+ except Exception as e:
1168
+ print(f"Export users error: {e}")
1169
+ return jsonify({'error': 'Failed to export users'}), 500
1170
+
1171
+ @app.route('/admin/export/tokens', methods=['GET'])
1172
+ @admin_required
1173
+ def export_token_usage():
1174
+ """Export token usage data as CSV"""
1175
+ try:
1176
+ import csv
1177
+ from io import StringIO
1178
+
1179
+ conn = get_db_connection()
1180
+ cursor = conn.cursor()
1181
+
1182
+ cursor.execute("""
1183
+ SELECT t.created_at, u.email, t.api_provider, t.input_tokens,
1184
+ t.output_tokens, t.tokens_used, t.operation_type
1185
+ FROM token_usage t
1186
+ LEFT JOIN users u ON t.user_id = u.id
1187
+ ORDER BY t.created_at DESC
1188
+ """)
1189
+
1190
+ token_data = cursor.fetchall()
1191
+ conn.close()
1192
+
1193
+ output = StringIO()
1194
+ writer = csv.writer(output)
1195
+
1196
+ # Write header
1197
+ writer.writerow(['Date', 'User Email', 'Provider', 'Input Tokens', 'Output Tokens', 'Total Tokens', 'Operation'])
1198
+
1199
+ # Write data
1200
+ for row in token_data:
1201
+ writer.writerow(row)
1202
+
1203
+ output.seek(0)
1204
+
1205
+ return Response(
1206
+ output.getvalue(),
1207
+ mimetype='text/csv',
1208
+ headers={'Content-Disposition': f'attachment; filename=token_usage_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'}
1209
+ )
1210
+
1211
+ except Exception as e:
1212
+ print(f"Export tokens error: {e}")
1213
+ return jsonify({'error': 'Failed to export token usage'}), 500
1214
+
1215
+ @app.route('/admin/system/health', methods=['GET'])
1216
+ @admin_required
1217
+ def get_system_health():
1218
+ """Get system health metrics"""
1219
+ try:
1220
+ health = admin_manager.get_system_health()
1221
+ return jsonify({'success': True, 'health': health})
1222
+ except Exception as e:
1223
+ print(f"System health error: {e}")
1224
+ return jsonify({'error': 'Failed to get system health'}), 500
1225
+
1226
+ @app.route('/admin/system/alerts', methods=['GET'])
1227
+ @admin_required
1228
+ def get_system_alerts():
1229
+ """Get system alerts"""
1230
+ try:
1231
+ alerts = admin_manager.check_system_alerts()
1232
+ return jsonify({'success': True, 'alerts': alerts})
1233
+ except Exception as e:
1234
+ print(f"System alerts error: {e}")
1235
+ return jsonify({'error': 'Failed to get system alerts'}), 500
1236
+
1237
+ @app.route('/admin')
1238
+ def admin_interface():
1239
+ """Serve admin interface"""
1240
+ return send_file('admin.html')
1241
+
1242
+ @app.route('/')
1243
+ def root():
1244
+ return send_file('index.html')
1245
+
1246
+ if __name__ == '__main__':
1247
+ app.run(host='0.0.0.0', port=7860)
content_curator.py ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # content_curator.py
2
+ import requests
3
+ import json
4
+ import re
5
+ from datetime import datetime, timedelta
6
+ from urllib.parse import urlparse, urljoin
7
+ from bs4 import BeautifulSoup
8
+ import feedparser
9
+ import logging
10
+ from groq import Groq
11
+ import google.generativeai as genai
12
+ import os
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Initialize AI clients
17
+ groq_client = None
18
+ genai_client = None
19
+
20
+ try:
21
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
22
+ if GROQ_API_KEY:
23
+ groq_client = Groq(api_key=GROQ_API_KEY)
24
+ except Exception as e:
25
+ logger.warning(f"Groq client not available: {e}")
26
+
27
+ try:
28
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
29
+ if GEMINI_API_KEY:
30
+ genai.configure(api_key=GEMINI_API_KEY)
31
+ genai_client = genai
32
+ except Exception as e:
33
+ logger.warning(f"Gemini client not available: {e}")
34
+
35
+ class ContentCurator:
36
+ def __init__(self):
37
+ # Import here to avoid circular imports
38
+ self.track_tokens = None
39
+ try:
40
+ from admin_module import admin_manager
41
+ self.admin_manager = admin_manager
42
+ except ImportError:
43
+ self.admin_manager = None
44
+
45
+ def _track_token_usage(self, user_id, provider, input_tokens, output_tokens, operation):
46
+ """Track token usage for admin monitoring"""
47
+ try:
48
+ if self.admin_manager:
49
+ self.admin_manager.record_token_usage(user_id, provider, input_tokens, output_tokens, operation)
50
+ except Exception as e:
51
+ logger.error(f"Error tracking tokens: {e}")
52
+
53
+ self.search_engines = {
54
+ # Using free APIs and web scraping
55
+ 'news_sources': {
56
+ 'bbc': 'https://feeds.bbci.co.uk/news/rss.xml',
57
+ 'reuters': 'https://www.reuters.com/arcio/rss/',
58
+ 'techcrunch': 'https://techcrunch.com/feed/',
59
+ 'medium': 'https://medium.com/feed/tag/{topic}',
60
+ },
61
+ 'content_categories': {
62
+ 'technology': ['tech', 'software', 'AI', 'cybersecurity', 'automotive'],
63
+ 'business': ['business', 'management', 'leadership', 'finance'],
64
+ 'science': ['science', 'research', 'innovation'],
65
+ 'professional': ['career', 'professional-development', 'skills']
66
+ }
67
+ }
68
+
69
+ def search_content(self, interests, english_level, context_focus, limit=10):
70
+ """Search for content based on user interests and level"""
71
+ try:
72
+ results = []
73
+
74
+ for interest in interests:
75
+ # Search RSS feeds
76
+ rss_results = self._search_rss_feeds(interest, limit=3)
77
+ results.extend(rss_results)
78
+
79
+ # Search Medium articles
80
+ medium_results = self._search_medium(interest, limit=2)
81
+ results.extend(medium_results)
82
+
83
+ # Filter and rank results
84
+ filtered_results = self._filter_by_level_and_context(
85
+ results, english_level, context_focus
86
+ )
87
+
88
+ return filtered_results[:limit]
89
+
90
+ except Exception as e:
91
+ logger.error(f"Error searching content: {e}")
92
+ return []
93
+
94
+ def _search_rss_feeds(self, topic, limit=5):
95
+ """Search RSS feeds for relevant content"""
96
+ results = []
97
+
98
+ try:
99
+ # Map topic to appropriate RSS feeds
100
+ relevant_feeds = []
101
+
102
+ if any(keyword in topic.lower() for keyword in ['tech', 'software', 'cyber', 'adas', 'automotive']):
103
+ relevant_feeds.extend([
104
+ 'https://feeds.bbci.co.uk/news/technology/rss.xml',
105
+ 'https://techcrunch.com/feed/',
106
+ 'https://www.wired.com/feed/rss'
107
+ ])
108
+
109
+ if any(keyword in topic.lower() for keyword in ['business', 'management', 'product']):
110
+ relevant_feeds.extend([
111
+ 'https://feeds.bbci.co.uk/news/business/rss.xml',
112
+ 'https://feeds.harvard.edu/news/rss/business.xml'
113
+ ])
114
+
115
+ # Default to general news if no specific match
116
+ if not relevant_feeds:
117
+ relevant_feeds = ['https://feeds.bbci.co.uk/news/rss.xml']
118
+
119
+ for feed_url in relevant_feeds[:2]: # Limit to 2 feeds to avoid timeout
120
+ try:
121
+ feed = feedparser.parse(feed_url)
122
+
123
+ for entry in feed.entries[:limit]:
124
+ if self._is_relevant_to_topic(entry.title + " " + entry.get('summary', ''), topic):
125
+ results.append({
126
+ 'title': entry.title,
127
+ 'url': entry.link,
128
+ 'summary': entry.get('summary', '')[:200] + '...',
129
+ 'source': urlparse(feed_url).netloc,
130
+ 'published': entry.get('published', ''),
131
+ 'relevance_score': self._calculate_relevance(entry.title, topic)
132
+ })
133
+
134
+ except Exception as e:
135
+ logger.warning(f"Error parsing feed {feed_url}: {e}")
136
+ continue
137
+
138
+ except Exception as e:
139
+ logger.error(f"Error in RSS search: {e}")
140
+
141
+ return sorted(results, key=lambda x: x['relevance_score'], reverse=True)
142
+
143
+ def _search_medium(self, topic, limit=3):
144
+ """Search Medium articles (simplified approach)"""
145
+ results = []
146
+
147
+ try:
148
+ # Use Medium's RSS feed for topics
149
+ search_terms = topic.lower().replace(' ', '-')
150
+ medium_url = f"https://medium.com/feed/tag/{search_terms}"
151
+
152
+ feed = feedparser.parse(medium_url)
153
+
154
+ for entry in feed.entries[:limit]:
155
+ results.append({
156
+ 'title': entry.title,
157
+ 'url': entry.link,
158
+ 'summary': entry.get('summary', '')[:200] + '...',
159
+ 'source': 'Medium',
160
+ 'published': entry.get('published', ''),
161
+ 'relevance_score': self._calculate_relevance(entry.title, topic)
162
+ })
163
+
164
+ except Exception as e:
165
+ logger.warning(f"Error searching Medium: {e}")
166
+
167
+ return results
168
+
169
+ def _is_relevant_to_topic(self, text, topic):
170
+ """Check if text is relevant to topic"""
171
+ text_lower = text.lower()
172
+ topic_words = topic.lower().split()
173
+
174
+ # Simple relevance check
175
+ matches = sum(1 for word in topic_words if word in text_lower)
176
+ return matches >= len(topic_words) * 0.5 # At least 50% of topic words present
177
+
178
+ def _calculate_relevance(self, title, topic):
179
+ """Calculate relevance score between title and topic"""
180
+ title_lower = title.lower()
181
+ topic_lower = topic.lower()
182
+
183
+ # Simple scoring based on word matches
184
+ topic_words = topic_lower.split()
185
+ score = 0
186
+
187
+ for word in topic_words:
188
+ if word in title_lower:
189
+ score += 1
190
+
191
+ return score / len(topic_words) if topic_words else 0
192
+
193
+ def _filter_by_level_and_context(self, results, english_level, context_focus):
194
+ """Filter results by English level and context"""
195
+ # Level difficulty mapping
196
+ level_complexity = {
197
+ 'A1': 1, 'A2': 2, 'B1': 3, 'B2': 4, 'C1': 5, 'C2': 6
198
+ }
199
+
200
+ user_level = level_complexity.get(english_level, 3)
201
+
202
+ filtered = []
203
+ for result in results:
204
+ # Estimate content difficulty (simplified)
205
+ difficulty = self._estimate_content_difficulty(result['title'] + " " + result['summary'])
206
+
207
+ # Filter by level (allow content slightly above user level)
208
+ if difficulty <= user_level + 1:
209
+ result['estimated_difficulty'] = difficulty
210
+ filtered.append(result)
211
+
212
+ return filtered
213
+
214
+ def _estimate_content_difficulty(self, text):
215
+ """Estimate content difficulty (1-6 scale)"""
216
+ # Simple heuristics for difficulty estimation
217
+ word_count = len(text.split())
218
+ avg_word_length = sum(len(word) for word in text.split()) / word_count if word_count > 0 else 0
219
+
220
+ # Technical terms increase difficulty
221
+ technical_terms = ['algorithm', 'implementation', 'architecture', 'methodology', 'paradigm']
222
+ tech_score = sum(1 for term in technical_terms if term in text.lower())
223
+
224
+ # Calculate difficulty score
225
+ difficulty = 1
226
+ if avg_word_length > 6:
227
+ difficulty += 1
228
+ if tech_score > 0:
229
+ difficulty += 1
230
+ if word_count > 200:
231
+ difficulty += 1
232
+
233
+ return min(difficulty, 6)
234
+
235
+ def extract_content_from_url(self, url):
236
+ """Extract readable content from URL"""
237
+ try:
238
+ headers = {
239
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
240
+ }
241
+
242
+ response = requests.get(url, headers=headers, timeout=10)
243
+ response.raise_for_status()
244
+
245
+ soup = BeautifulSoup(response.content, 'html.parser')
246
+
247
+ # Remove unwanted elements
248
+ for element in soup(['script', 'style', 'nav', 'header', 'footer', 'aside']):
249
+ element.decompose()
250
+
251
+ # Extract main content
252
+ main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=re.compile(r'content|article|post'))
253
+
254
+ if main_content:
255
+ text = main_content.get_text(separator=' ', strip=True)
256
+ else:
257
+ # Fallback to body text
258
+ text = soup.get_text(separator=' ', strip=True)
259
+
260
+ # Clean up text
261
+ text = re.sub(r'\s+', ' ', text) # Multiple spaces to single
262
+ text = text[:5000] # Limit length
263
+
264
+ return {
265
+ 'success': True,
266
+ 'content': text,
267
+ 'title': soup.find('title').text if soup.find('title') else '',
268
+ 'word_count': len(text.split())
269
+ }
270
+
271
+ except Exception as e:
272
+ logger.error(f"Error extracting content from {url}: {e}")
273
+ return {
274
+ 'success': False,
275
+ 'error': str(e)
276
+ }
277
+
278
+ def generate_personalized_recommendations(self, user_interests, recent_articles, english_level, context_focus, user_id=None):
279
+ """Generate AI-powered content recommendations"""
280
+ try:
281
+ if not groq_client and not genai_client:
282
+ return []
283
+
284
+ # Prepare context for AI
285
+ interests_text = ', '.join(user_interests.keys())
286
+ recent_titles = [article.get('title', '') for article in recent_articles[-5:]]
287
+ recent_text = '; '.join(recent_titles)
288
+
289
+ prompt = f"""
290
+ User Profile:
291
+ - English Level: {english_level}
292
+ - Context Focus: {context_focus}
293
+ - Interests: {interests_text}
294
+ - Recently read: {recent_text}
295
+
296
+ Recommend 5 specific article topics or search terms that would be perfect for this user's English learning journey.
297
+ Consider their level and interests. Focus on practical, engaging content.
298
+
299
+ Format as JSON array: [
300
+ {{"topic": "topic name", "reason": "why this is good for the user", "difficulty": "estimated level"}},
301
+ ...
302
+ ]
303
+ """
304
+
305
+ # Try Groq first, then Gemini
306
+ response_text = None
307
+ if groq_client:
308
+ response = groq_client.chat.completions.create(
309
+ model="llama-3.1-8b-instant",
310
+ messages=[{"role": "user", "content": prompt}],
311
+ temperature=0.7
312
+ )
313
+ response_text = response.choices[0].message.content
314
+
315
+ # Track token usage
316
+ if user_id and hasattr(response, 'usage'):
317
+ self._track_token_usage(
318
+ user_id, 'groq',
319
+ response.usage.prompt_tokens,
320
+ response.usage.completion_tokens,
321
+ 'content_recommendations'
322
+ )
323
+ elif genai_client:
324
+ model = genai_client.GenerativeModel('gemini-2.5-flash-latest')
325
+ response = model.generate_content(prompt)
326
+ response_text = response.text
327
+
328
+ # Track token usage for Gemini (estimated)
329
+ if user_id:
330
+ # Estimate tokens (rough approximation: 1 token β‰ˆ 4 characters)
331
+ input_tokens = len(prompt) // 4
332
+ output_tokens = len(response_text) // 4
333
+ self._track_token_usage(
334
+ user_id, 'gemini',
335
+ input_tokens,
336
+ output_tokens,
337
+ 'content_recommendations'
338
+ )
339
+
340
+ if response_text:
341
+ # Extract JSON from response
342
+ json_match = re.search(r'\[.*\]', response_text, re.DOTALL)
343
+ if json_match:
344
+ recommendations = json.loads(json_match.group())
345
+ return recommendations
346
+
347
+ return []
348
+
349
+ except Exception as e:
350
+ logger.error(f"Error generating recommendations: {e}")
351
+ return []
352
+
353
+ def analyze_content_for_learning(self, content, user_level):
354
+ """Analyze content and suggest learning points"""
355
+ try:
356
+ if not groq_client and not genai_client:
357
+ return {}
358
+
359
+ # Truncate content for analysis
360
+ analysis_content = content[:2000] + "..." if len(content) > 2000 else content
361
+
362
+ prompt = f"""
363
+ Analyze this English text for a {user_level} level English learner:
364
+
365
+ "{analysis_content}"
366
+
367
+ Provide:
368
+ 1. Key vocabulary words (5-8 words) with definitions
369
+ 2. Important grammar patterns used
370
+ 3. Main topics/themes
371
+ 4. Difficulty assessment (1-10)
372
+ 5. Learning suggestions for this level
373
+
374
+ Format as JSON: {{
375
+ "vocabulary": [{{"word": "...", "definition": "..."}}, ...],
376
+ "grammar_patterns": ["pattern1", "pattern2", ...],
377
+ "topics": ["topic1", "topic2", ...],
378
+ "difficulty": 7,
379
+ "learning_suggestions": ["suggestion1", "suggestion2", ...]
380
+ }}
381
+ """
382
+
383
+ response_text = None
384
+ if groq_client:
385
+ response = groq_client.chat.completions.create(
386
+ model="llama-3.1-8b-instant",
387
+ messages=[{"role": "user", "content": prompt}],
388
+ temperature=0.3
389
+ )
390
+ response_text = response.choices[0].message.content
391
+ elif genai_client:
392
+ model = genai_client.GenerativeModel('gemini-2.5-flash-latest')
393
+ response = model.generate_content(prompt)
394
+ response_text = response.text
395
+
396
+ if response_text:
397
+ json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
398
+ if json_match:
399
+ analysis = json.loads(json_match.group())
400
+ return analysis
401
+
402
+ return {}
403
+
404
+ except Exception as e:
405
+ logger.error(f"Error analyzing content: {e}")
406
+ return {}
407
+
408
+ # Global instance
409
+ content_curator = ContentCurator()
database.py ADDED
@@ -0,0 +1,744 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # database.py
2
+ import sqlite3
3
+ import os
4
+ import hashlib
5
+ import secrets
6
+ import smtplib
7
+ from email.mime.text import MIMEText
8
+ from email.mime.multipart import MIMEMultipart
9
+ from datetime import datetime, timedelta
10
+ from functools import wraps
11
+ from flask import g, session, jsonify, request
12
+ import logging
13
+
14
+ # Configure logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Database configuration
19
+ DATABASE_PATH = 'users.db'
20
+
21
+ def get_db():
22
+ """Get database connection"""
23
+ if 'db' not in g:
24
+ g.db = sqlite3.connect(DATABASE_PATH)
25
+ g.db.row_factory = sqlite3.Row
26
+ return g.db
27
+
28
+ def close_db(e=None):
29
+ """Close database connection"""
30
+ db = g.pop('db', None)
31
+ if db is not None:
32
+ db.close()
33
+
34
+ def init_db():
35
+ """Initialize database with required tables"""
36
+ try:
37
+ db = sqlite3.connect(DATABASE_PATH)
38
+ db.executescript('''
39
+ -- Users table
40
+ CREATE TABLE IF NOT EXISTS users (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ email TEXT UNIQUE NOT NULL,
43
+ password_hash TEXT NOT NULL,
44
+ salt TEXT NOT NULL,
45
+ is_confirmed BOOLEAN DEFAULT FALSE,
46
+ confirmation_token TEXT,
47
+ reset_token TEXT,
48
+ reset_token_expires TIMESTAMP,
49
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
50
+ last_login TIMESTAMP,
51
+ is_active BOOLEAN DEFAULT TRUE
52
+ );
53
+
54
+ -- User flashcards table
55
+ CREATE TABLE IF NOT EXISTS user_flashcards (
56
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ user_id INTEGER NOT NULL,
58
+ term TEXT NOT NULL,
59
+ translation TEXT,
60
+ context_sentence TEXT,
61
+ gapped_sentence TEXT,
62
+ definition TEXT,
63
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
64
+ study_count INTEGER DEFAULT 0,
65
+ last_studied TIMESTAMP,
66
+ difficulty_level INTEGER DEFAULT 1,
67
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
68
+ );
69
+
70
+ -- User study sessions table
71
+ CREATE TABLE IF NOT EXISTS study_sessions (
72
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
73
+ user_id INTEGER NOT NULL,
74
+ session_type TEXT NOT NULL, -- 'flashcard', 'conversation', 'activity'
75
+ duration_minutes INTEGER,
76
+ cards_studied INTEGER DEFAULT 0,
77
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
78
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
79
+ );
80
+
81
+ -- User settings table
82
+ CREATE TABLE IF NOT EXISTS user_settings (
83
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
84
+ user_id INTEGER UNIQUE NOT NULL,
85
+ preferred_model TEXT DEFAULT 'gemini:gemini-2.5-flash-latest',
86
+ context_focus TEXT DEFAULT 'General/Social',
87
+ voice_accent TEXT DEFAULT 'co.uk',
88
+ daily_goal INTEGER DEFAULT 10,
89
+ notification_enabled BOOLEAN DEFAULT TRUE,
90
+ -- New settings for advanced features
91
+ english_level TEXT DEFAULT 'B1', -- A1, A2, B1, B2, C1, C2
92
+ study_goals TEXT, -- JSON: objectives like "business english", "technical vocabulary"
93
+ preferred_content_types TEXT DEFAULT 'articles,videos', -- comma separated
94
+ content_difficulty TEXT DEFAULT 'adaptive', -- 'easy', 'medium', 'hard', 'adaptive'
95
+ study_schedule TEXT, -- JSON: preferred days/times
96
+ auto_recommendations BOOLEAN DEFAULT TRUE,
97
+ content_sources TEXT DEFAULT 'news,tech,business', -- preferred content sources
98
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
99
+ );
100
+
101
+ -- User articles/content table
102
+ CREATE TABLE IF NOT EXISTS user_articles (
103
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ user_id INTEGER NOT NULL,
105
+ title TEXT NOT NULL,
106
+ content TEXT NOT NULL,
107
+ source_url TEXT,
108
+ source_type TEXT DEFAULT 'manual', -- 'manual', 'web_search', 'recommended'
109
+ category TEXT, -- user interest category
110
+ difficulty_level TEXT, -- estimated difficulty
111
+ word_count INTEGER,
112
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
113
+ last_accessed TIMESTAMP,
114
+ is_favorite BOOLEAN DEFAULT FALSE,
115
+ study_progress REAL DEFAULT 0.0, -- 0.0 to 1.0
116
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
117
+ );
118
+
119
+ -- User interests/preferences table
120
+ CREATE TABLE IF NOT EXISTS user_interests (
121
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
122
+ user_id INTEGER NOT NULL,
123
+ interest_category TEXT NOT NULL,
124
+ weight REAL DEFAULT 1.0, -- importance weight
125
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
126
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
127
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
128
+ UNIQUE(user_id, interest_category)
129
+ );
130
+
131
+ -- Content recommendations table
132
+ CREATE TABLE IF NOT EXISTS content_recommendations (
133
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
134
+ user_id INTEGER NOT NULL,
135
+ article_id INTEGER,
136
+ recommendation_reason TEXT,
137
+ relevance_score REAL,
138
+ recommended_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
139
+ viewed BOOLEAN DEFAULT FALSE,
140
+ accepted BOOLEAN DEFAULT FALSE,
141
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
142
+ FOREIGN KEY (article_id) REFERENCES user_articles (id) ON DELETE CASCADE
143
+ );
144
+
145
+ -- Study plans table
146
+ CREATE TABLE IF NOT EXISTS study_plans (
147
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
148
+ user_id INTEGER NOT NULL,
149
+ plan_name TEXT NOT NULL,
150
+ target_level TEXT, -- A1, A2, B1, B2, C1, C2
151
+ current_level TEXT,
152
+ objectives TEXT, -- JSON string with objectives
153
+ weekly_hours INTEGER DEFAULT 5,
154
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
155
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
156
+ is_active BOOLEAN DEFAULT TRUE,
157
+ completion_percentage REAL DEFAULT 0.0,
158
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
159
+ );
160
+
161
+ -- Study plan activities table
162
+ CREATE TABLE IF NOT EXISTS study_plan_activities (
163
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
164
+ plan_id INTEGER NOT NULL,
165
+ activity_type TEXT NOT NULL, -- 'reading', 'flashcards', 'conversation', 'writing'
166
+ content_reference TEXT, -- reference to article, flashcard set, etc.
167
+ scheduled_date DATE,
168
+ estimated_duration INTEGER, -- minutes
169
+ actual_duration INTEGER,
170
+ completed BOOLEAN DEFAULT FALSE,
171
+ completed_at TIMESTAMP,
172
+ difficulty_rating INTEGER, -- 1-5 user rating
173
+ notes TEXT,
174
+ FOREIGN KEY (plan_id) REFERENCES study_plans (id) ON DELETE CASCADE
175
+ );
176
+
177
+ -- User analytics table
178
+ CREATE TABLE IF NOT EXISTS user_analytics (
179
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
180
+ user_id INTEGER NOT NULL,
181
+ metric_name TEXT NOT NULL,
182
+ metric_value REAL NOT NULL,
183
+ metric_date DATE NOT NULL,
184
+ context_data TEXT, -- JSON with additional context
185
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
186
+ );
187
+
188
+ -- Token usage tracking table for admin
189
+ CREATE TABLE IF NOT EXISTS token_usage (
190
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
191
+ user_id INTEGER,
192
+ api_provider TEXT NOT NULL, -- 'groq', 'gemini', etc.
193
+ input_tokens INTEGER DEFAULT 0,
194
+ output_tokens INTEGER DEFAULT 0,
195
+ operation_type TEXT, -- 'conversation', 'content_analysis', 'recommendation', etc.
196
+ tokens_used INTEGER GENERATED ALWAYS AS (input_tokens + output_tokens) STORED,
197
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
198
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
199
+ );
200
+
201
+ -- Create indexes for better performance
202
+ CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
203
+ CREATE INDEX IF NOT EXISTS idx_users_confirmation_token ON users(confirmation_token);
204
+ CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(reset_token);
205
+ CREATE INDEX IF NOT EXISTS idx_flashcards_user_id ON user_flashcards(user_id);
206
+ CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON study_sessions(user_id);
207
+ CREATE INDEX IF NOT EXISTS idx_settings_user_id ON user_settings(user_id);
208
+ CREATE INDEX IF NOT EXISTS idx_articles_user_id ON user_articles(user_id);
209
+ CREATE INDEX IF NOT EXISTS idx_articles_category ON user_articles(category);
210
+ CREATE INDEX IF NOT EXISTS idx_interests_user_id ON user_interests(user_id);
211
+ CREATE INDEX IF NOT EXISTS idx_recommendations_user_id ON content_recommendations(user_id);
212
+ CREATE INDEX IF NOT EXISTS idx_study_plans_user_id ON study_plans(user_id);
213
+ CREATE INDEX IF NOT EXISTS idx_plan_activities_plan_id ON study_plan_activities(plan_id);
214
+ CREATE INDEX IF NOT EXISTS idx_analytics_user_date ON user_analytics(user_id, metric_date);
215
+ CREATE INDEX IF NOT EXISTS idx_token_usage_user_id ON token_usage(user_id);
216
+ CREATE INDEX IF NOT EXISTS idx_token_usage_provider ON token_usage(api_provider);
217
+ CREATE INDEX IF NOT EXISTS idx_token_usage_date ON token_usage(created_at);
218
+ ''')
219
+ db.commit()
220
+ db.close()
221
+ logger.info("Database initialized successfully")
222
+ return True
223
+ except Exception as e:
224
+ logger.error(f"Error initializing database: {e}")
225
+ return False
226
+
227
+ def hash_password(password, salt=None):
228
+ """Hash password with salt"""
229
+ if salt is None:
230
+ salt = secrets.token_hex(32)
231
+
232
+ password_hash = hashlib.pbkdf2_hmac(
233
+ 'sha256',
234
+ password.encode('utf-8'),
235
+ salt.encode('utf-8'),
236
+ 100000 # iterations
237
+ )
238
+ return password_hash.hex(), salt
239
+
240
+ def verify_password(password, password_hash, salt):
241
+ """Verify password against hash"""
242
+ new_hash, _ = hash_password(password, salt)
243
+ return new_hash == password_hash
244
+
245
+ def generate_token():
246
+ """Generate secure random token"""
247
+ return secrets.token_urlsafe(32)
248
+
249
+ def create_user(email, password):
250
+ """Create new user account"""
251
+ try:
252
+ db = get_db()
253
+
254
+ # Check if user already exists
255
+ existing_user = db.execute(
256
+ 'SELECT id FROM users WHERE email = ?', (email,)
257
+ ).fetchone()
258
+
259
+ if existing_user:
260
+ return {'success': False, 'message': 'Email already registered'}
261
+
262
+ # Hash password
263
+ password_hash, salt = hash_password(password)
264
+ confirmation_token = generate_token()
265
+
266
+ # Insert user
267
+ cursor = db.execute(
268
+ '''INSERT INTO users (email, password_hash, salt, confirmation_token)
269
+ VALUES (?, ?, ?, ?)''',
270
+ (email, password_hash, salt, confirmation_token)
271
+ )
272
+ user_id = cursor.lastrowid
273
+
274
+ # Create default user settings
275
+ db.execute(
276
+ '''INSERT INTO user_settings (user_id) VALUES (?)''',
277
+ (user_id,)
278
+ )
279
+
280
+ db.commit()
281
+
282
+ logger.info(f"User created: {email}")
283
+ return {
284
+ 'success': True,
285
+ 'user_id': user_id,
286
+ 'confirmation_token': confirmation_token,
287
+ 'message': 'User created successfully'
288
+ }
289
+
290
+ except Exception as e:
291
+ logger.error(f"Error creating user: {e}")
292
+ return {'success': False, 'message': 'Internal server error'}
293
+
294
+ def authenticate_user(email, password):
295
+ """Authenticate user login"""
296
+ try:
297
+ db = get_db()
298
+ user = db.execute(
299
+ '''SELECT id, email, password_hash, salt, is_confirmed, is_active
300
+ FROM users WHERE email = ?''', (email,)
301
+ ).fetchone()
302
+
303
+ if not user:
304
+ return {'success': False, 'message': 'Invalid email or password'}
305
+
306
+ if not user['is_active']:
307
+ return {'success': False, 'message': 'Account is deactivated'}
308
+
309
+ if not verify_password(password, user['password_hash'], user['salt']):
310
+ return {'success': False, 'message': 'Invalid email or password'}
311
+
312
+ if not user['is_confirmed']:
313
+ return {'success': False, 'message': 'Please confirm your email before logging in'}
314
+
315
+ # Update last login
316
+ db.execute(
317
+ 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?',
318
+ (user['id'],)
319
+ )
320
+ db.commit()
321
+
322
+ return {
323
+ 'success': True,
324
+ 'user_id': user['id'],
325
+ 'email': user['email'],
326
+ 'message': 'Login successful'
327
+ }
328
+
329
+ except Exception as e:
330
+ logger.error(f"Error authenticating user: {e}")
331
+ return {'success': False, 'message': 'Internal server error'}
332
+
333
+ def confirm_email(token):
334
+ """Confirm user email with token"""
335
+ try:
336
+ db = get_db()
337
+ user = db.execute(
338
+ 'SELECT id, email FROM users WHERE confirmation_token = ? AND is_confirmed = FALSE',
339
+ (token,)
340
+ ).fetchone()
341
+
342
+ if not user:
343
+ return {'success': False, 'message': 'Invalid or expired confirmation token'}
344
+
345
+ db.execute(
346
+ '''UPDATE users SET is_confirmed = TRUE, confirmation_token = NULL
347
+ WHERE id = ?''',
348
+ (user['id'],)
349
+ )
350
+ db.commit()
351
+
352
+ logger.info(f"Email confirmed for user: {user['email']}")
353
+ return {'success': True, 'message': 'Email confirmed successfully'}
354
+
355
+ except Exception as e:
356
+ logger.error(f"Error confirming email: {e}")
357
+ return {'success': False, 'message': 'Internal server error'}
358
+
359
+ def get_user_settings(user_id):
360
+ """Get user settings"""
361
+ try:
362
+ db = get_db()
363
+ settings = db.execute(
364
+ '''SELECT preferred_model, context_focus, voice_accent, daily_goal, notification_enabled
365
+ FROM user_settings WHERE user_id = ?''',
366
+ (user_id,)
367
+ ).fetchone()
368
+
369
+ if settings:
370
+ return dict(settings)
371
+ return None
372
+
373
+ except Exception as e:
374
+ logger.error(f"Error getting user settings: {e}")
375
+ return None
376
+
377
+ def update_user_settings(user_id, settings):
378
+ """Update user settings"""
379
+ try:
380
+ db = get_db()
381
+ db.execute(
382
+ '''UPDATE user_settings
383
+ SET preferred_model = ?, context_focus = ?, voice_accent = ?,
384
+ daily_goal = ?, notification_enabled = ?
385
+ WHERE user_id = ?''',
386
+ (settings.get('preferred_model'), settings.get('context_focus'),
387
+ settings.get('voice_accent'), settings.get('daily_goal'),
388
+ settings.get('notification_enabled'), user_id)
389
+ )
390
+ db.commit()
391
+ return True
392
+
393
+ except Exception as e:
394
+ logger.error(f"Error updating user settings: {e}")
395
+ return False
396
+
397
+ def save_user_flashcard(user_id, flashcard_data):
398
+ """Save flashcard to user's collection"""
399
+ try:
400
+ db = get_db()
401
+ db.execute(
402
+ '''INSERT INTO user_flashcards
403
+ (user_id, term, translation, context_sentence, gapped_sentence, definition)
404
+ VALUES (?, ?, ?, ?, ?, ?)''',
405
+ (user_id, flashcard_data.get('term'), flashcard_data.get('translation'),
406
+ flashcard_data.get('context_sentence'), flashcard_data.get('gapped_sentence'),
407
+ flashcard_data.get('definition'))
408
+ )
409
+ db.commit()
410
+ return True
411
+
412
+ except Exception as e:
413
+ logger.error(f"Error saving flashcard: {e}")
414
+ return False
415
+
416
+ def get_user_flashcards(user_id, limit=50):
417
+ """Get user's flashcards"""
418
+ try:
419
+ db = get_db()
420
+ flashcards = db.execute(
421
+ '''SELECT * FROM user_flashcards
422
+ WHERE user_id = ?
423
+ ORDER BY created_at DESC
424
+ LIMIT ?''',
425
+ (user_id, limit)
426
+ ).fetchall()
427
+
428
+ return [dict(card) for card in flashcards]
429
+
430
+ except Exception as e:
431
+ logger.error(f"Error getting user flashcards: {e}")
432
+ return []
433
+
434
+ def record_study_session(user_id, session_type, duration_minutes=None, cards_studied=0):
435
+ """Record a study session"""
436
+ try:
437
+ db = get_db()
438
+ db.execute(
439
+ '''INSERT INTO study_sessions (user_id, session_type, duration_minutes, cards_studied)
440
+ VALUES (?, ?, ?, ?)''',
441
+ (user_id, session_type, duration_minutes, cards_studied)
442
+ )
443
+ db.commit()
444
+ return True
445
+
446
+ except Exception as e:
447
+ logger.error(f"Error recording study session: {e}")
448
+ return False
449
+
450
+ # Authentication decorators
451
+ def login_required(f):
452
+ """Decorator to require login"""
453
+ @wraps(f)
454
+ def decorated_function(*args, **kwargs):
455
+ if 'user_id' not in session:
456
+ return jsonify({'error': 'Authentication required'}), 401
457
+ return f(*args, **kwargs)
458
+ return decorated_function
459
+
460
+ def get_current_user():
461
+ """Get current logged in user"""
462
+ if 'user_id' in session:
463
+ try:
464
+ db = get_db()
465
+ user = db.execute(
466
+ 'SELECT id, email, is_confirmed FROM users WHERE id = ? AND is_active = TRUE',
467
+ (session['user_id'],)
468
+ ).fetchone()
469
+ return dict(user) if user else None
470
+ except Exception as e:
471
+ logger.error(f"Error getting current user: {e}")
472
+ return None
473
+ return None
474
+
475
+ # Email functionality (for Hugging Face Spaces)
476
+ def send_confirmation_email(email, token):
477
+ """Send confirmation email (simplified for HF Spaces)"""
478
+ try:
479
+ # For Hugging Face Spaces, we'll use environment variables for SMTP
480
+ smtp_server = os.environ.get('SMTP_SERVER', 'smtp.gmail.com')
481
+ smtp_port = int(os.environ.get('SMTP_PORT', '587'))
482
+ smtp_username = os.environ.get('SMTP_USERNAME')
483
+ smtp_password = os.environ.get('SMTP_PASSWORD')
484
+
485
+ if not all([smtp_username, smtp_password]):
486
+ logger.warning("SMTP credentials not configured")
487
+ return False
488
+
489
+ # Create confirmation URL (will be updated with actual domain)
490
+ base_url = os.environ.get('BASE_URL', 'http://localhost:7860')
491
+ confirm_url = f"{base_url}/confirm-email?token={token}"
492
+
493
+ # Create email
494
+ msg = MIMEMultipart()
495
+ msg['From'] = smtp_username
496
+ msg['To'] = email
497
+ msg['Subject'] = "Confirm your English Helper account"
498
+
499
+ body = f"""
500
+ <html>
501
+ <body>
502
+ <h2>Welcome to Dynamic English Study Studio!</h2>
503
+ <p>Thank you for creating an account. Please click the link below to confirm your email address:</p>
504
+ <p><a href="{confirm_url}" style="background-color: #4f46e5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Confirm Email</a></p>
505
+ <p>If the button doesn't work, copy and paste this link into your browser:</p>
506
+ <p>{confirm_url}</p>
507
+ <p>This link will expire in 24 hours.</p>
508
+ <p>If you didn't create this account, please ignore this email.</p>
509
+ </body>
510
+ </html>
511
+ """
512
+
513
+ msg.attach(MIMEText(body, 'html'))
514
+
515
+ # Send email
516
+ server = smtplib.SMTP(smtp_server, smtp_port)
517
+ server.starttls()
518
+ server.login(smtp_username, smtp_password)
519
+ text = msg.as_string()
520
+ server.sendmail(smtp_username, email, text)
521
+ server.quit()
522
+
523
+ logger.info(f"Confirmation email sent to {email}")
524
+ return True
525
+
526
+ except Exception as e:
527
+ logger.error(f"Error sending confirmation email: {e}")
528
+ return False
529
+
530
+ # --- CONTENT CURATION FUNCTIONS ---
531
+
532
+ def save_user_article(user_id, title, content, source_url=None, source_type='manual', category=None):
533
+ """Save article/content for user"""
534
+ try:
535
+ db = get_db()
536
+ word_count = len(content.split()) if content else 0
537
+
538
+ cursor = db.execute(
539
+ '''INSERT INTO user_articles
540
+ (user_id, title, content, source_url, source_type, category, word_count)
541
+ VALUES (?, ?, ?, ?, ?, ?, ?)''',
542
+ (user_id, title, content, source_url, source_type, category, word_count)
543
+ )
544
+ article_id = cursor.lastrowid
545
+ db.commit()
546
+
547
+ logger.info(f"Article saved for user {user_id}: {title}")
548
+ return {'success': True, 'article_id': article_id}
549
+
550
+ except Exception as e:
551
+ logger.error(f"Error saving article: {e}")
552
+ return {'success': False, 'message': 'Failed to save article'}
553
+
554
+ def get_user_articles(user_id, category=None, limit=50):
555
+ """Get user's saved articles"""
556
+ try:
557
+ db = get_db()
558
+
559
+ if category:
560
+ articles = db.execute(
561
+ '''SELECT * FROM user_articles
562
+ WHERE user_id = ? AND category = ?
563
+ ORDER BY created_at DESC LIMIT ?''',
564
+ (user_id, category, limit)
565
+ ).fetchall()
566
+ else:
567
+ articles = db.execute(
568
+ '''SELECT * FROM user_articles
569
+ WHERE user_id = ?
570
+ ORDER BY created_at DESC LIMIT ?''',
571
+ (user_id, limit)
572
+ ).fetchall()
573
+
574
+ return [dict(article) for article in articles]
575
+
576
+ except Exception as e:
577
+ logger.error(f"Error getting user articles: {e}")
578
+ return []
579
+
580
+ def update_user_interests(user_id, interests):
581
+ """Update user's interests/categories"""
582
+ try:
583
+ db = get_db()
584
+
585
+ # Clear existing interests
586
+ db.execute('DELETE FROM user_interests WHERE user_id = ?', (user_id,))
587
+
588
+ # Add new interests
589
+ for interest, weight in interests.items():
590
+ db.execute(
591
+ '''INSERT INTO user_interests (user_id, interest_category, weight)
592
+ VALUES (?, ?, ?)''',
593
+ (user_id, interest, weight)
594
+ )
595
+
596
+ db.commit()
597
+ return True
598
+
599
+ except Exception as e:
600
+ logger.error(f"Error updating user interests: {e}")
601
+ return False
602
+
603
+ def get_user_interests(user_id):
604
+ """Get user's interests"""
605
+ try:
606
+ db = get_db()
607
+ interests = db.execute(
608
+ 'SELECT interest_category, weight FROM user_interests WHERE user_id = ?',
609
+ (user_id,)
610
+ ).fetchall()
611
+
612
+ return {interest['interest_category']: interest['weight'] for interest in interests}
613
+
614
+ except Exception as e:
615
+ logger.error(f"Error getting user interests: {e}")
616
+ return {}
617
+
618
+ def create_study_plan(user_id, plan_name, target_level, current_level, objectives, weekly_hours=5):
619
+ """Create new study plan"""
620
+ try:
621
+ db = get_db()
622
+
623
+ cursor = db.execute(
624
+ '''INSERT INTO study_plans
625
+ (user_id, plan_name, target_level, current_level, objectives, weekly_hours)
626
+ VALUES (?, ?, ?, ?, ?, ?)''',
627
+ (user_id, plan_name, target_level, current_level, objectives, weekly_hours)
628
+ )
629
+ plan_id = cursor.lastrowid
630
+ db.commit()
631
+
632
+ logger.info(f"Study plan created for user {user_id}: {plan_name}")
633
+ return {'success': True, 'plan_id': plan_id}
634
+
635
+ except Exception as e:
636
+ logger.error(f"Error creating study plan: {e}")
637
+ return {'success': False, 'message': 'Failed to create study plan'}
638
+
639
+ def get_user_study_plans(user_id):
640
+ """Get user's study plans"""
641
+ try:
642
+ db = get_db()
643
+ plans = db.execute(
644
+ '''SELECT * FROM study_plans
645
+ WHERE user_id = ?
646
+ ORDER BY created_at DESC''',
647
+ (user_id,)
648
+ ).fetchall()
649
+
650
+ return [dict(plan) for plan in plans]
651
+
652
+ except Exception as e:
653
+ logger.error(f"Error getting study plans: {e}")
654
+ return []
655
+
656
+ def add_study_activity(plan_id, activity_type, content_reference, scheduled_date, estimated_duration):
657
+ """Add activity to study plan"""
658
+ try:
659
+ db = get_db()
660
+
661
+ db.execute(
662
+ '''INSERT INTO study_plan_activities
663
+ (plan_id, activity_type, content_reference, scheduled_date, estimated_duration)
664
+ VALUES (?, ?, ?, ?, ?)''',
665
+ (plan_id, activity_type, content_reference, scheduled_date, estimated_duration)
666
+ )
667
+ db.commit()
668
+ return True
669
+
670
+ except Exception as e:
671
+ logger.error(f"Error adding study activity: {e}")
672
+ return False
673
+
674
+ def get_study_activities(plan_id, date_range=None):
675
+ """Get activities for study plan"""
676
+ try:
677
+ db = get_db()
678
+
679
+ if date_range:
680
+ start_date, end_date = date_range
681
+ activities = db.execute(
682
+ '''SELECT * FROM study_plan_activities
683
+ WHERE plan_id = ? AND scheduled_date BETWEEN ? AND ?
684
+ ORDER BY scheduled_date''',
685
+ (plan_id, start_date, end_date)
686
+ ).fetchall()
687
+ else:
688
+ activities = db.execute(
689
+ '''SELECT * FROM study_plan_activities
690
+ WHERE plan_id = ?
691
+ ORDER BY scheduled_date''',
692
+ (plan_id,)
693
+ ).fetchall()
694
+
695
+ return [dict(activity) for activity in activities]
696
+
697
+ except Exception as e:
698
+ logger.error(f"Error getting study activities: {e}")
699
+ return []
700
+
701
+ def record_analytics_metric(user_id, metric_name, metric_value, context_data=None):
702
+ """Record analytics metric"""
703
+ try:
704
+ db = get_db()
705
+
706
+ db.execute(
707
+ '''INSERT INTO user_analytics (user_id, metric_name, metric_value, metric_date, context_data)
708
+ VALUES (?, ?, ?, DATE('now'), ?)''',
709
+ (user_id, metric_name, metric_value, context_data)
710
+ )
711
+ db.commit()
712
+ return True
713
+
714
+ except Exception as e:
715
+ logger.error(f"Error recording analytics: {e}")
716
+ return False
717
+
718
+ def get_user_analytics(user_id, metric_name=None, days=30):
719
+ """Get user analytics data"""
720
+ try:
721
+ db = get_db()
722
+
723
+ if metric_name:
724
+ analytics = db.execute(
725
+ '''SELECT * FROM user_analytics
726
+ WHERE user_id = ? AND metric_name = ?
727
+ AND metric_date >= DATE('now', '-{} days')
728
+ ORDER BY metric_date DESC'''.format(days),
729
+ (user_id, metric_name)
730
+ ).fetchall()
731
+ else:
732
+ analytics = db.execute(
733
+ '''SELECT * FROM user_analytics
734
+ WHERE user_id = ?
735
+ AND metric_date >= DATE('now', '-{} days')
736
+ ORDER BY metric_date DESC'''.format(days),
737
+ (user_id,)
738
+ ).fetchall()
739
+
740
+ return [dict(metric) for metric in analytics]
741
+
742
+ except Exception as e:
743
+ logger.error(f"Error getting analytics: {e}")
744
+ return []
index.html ADDED
@@ -0,0 +1,1959 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Dynamic English Study Studio</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
11
+ body { font-family: 'Inter', sans-serif; background-color: #f7f9fc; }
12
+ .card { background-color: white; border-radius: 12px; box-shadow: 0 4px_12px rgba(0,0,0,0.08); padding: 24px; }
13
+ .btn { padding: 0.5rem 1rem; border-radius: 8px; font-weight: 600; transition: all 0.2s; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
14
+ .btn:disabled { cursor: not-allowed; opacity: 0.5; }
15
+ .btn-primary { background-color: #4f46e5; color: white; }
16
+ .btn-primary:hover:not(:disabled) { background-color: #4338ca; }
17
+ .spinner { border: 2px solid rgba(255,255,255,0.3); width: 20px; height: 20px; border-radius: 50%; border-left-color: #fff; animation: spin 1s ease infinite; }
18
+ @keyframes spin { to { transform: rotate(360deg); } }
19
+ .flashcard-container { perspective: 1000px; position: relative; }
20
+ .flashcard-inner { position: relative; width: 100%; transition: transform 0.6s; transform-style: preserve-3d; }
21
+ .flashcard-container.flipped .flashcard-inner { transform: rotateY(180deg); }
22
+ .flashcard-front, .flashcard-back { position: absolute; width: 100%; height:100%; backface-visibility: hidden; border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; display: flex; flex-direction: column; justify-content: space-between; }
23
+ .flashcard-front { background-color: #f9fafb; }
24
+ .flashcard-back { background-color: #ffffff; transform: rotateY(180deg); }
25
+ .audio-btn { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #4f46e5; padding: 0; }
26
+ .delete-btn { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #9ca3af; z-index: 10; }
27
+ .delete-btn:hover { color: #ef4444; }
28
+ #activityChatDisplay { border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; min-height: 150px; background-color: #f9fafb; }
29
+ .ai-prompt { border-bottom: 2px dashed #d1d5db; padding-bottom: 1rem; margin-bottom: 1rem; }
30
+ .user-response-display { background-color: #eef2ff; border-left: 4px solid #4f46e5; padding: 0.75rem; margin-bottom: 1rem; font-style: italic; }
31
+ .ai-feedback { background-color: #f0fdf4; border-left: 4px solid #22c55e; padding: 0.75rem; }
32
+ .ai-feedback h3 { font-weight: 700; }
33
+
34
+ /* Toast Notifications */
35
+ .toast-container { position: fixed; top: 20px; right: 20px; z-index: 1000; max-width: 400px; }
36
+ .toast { background: white; border-left: 4px solid #4f46e5; border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.15); padding: 1rem; margin-bottom: 0.5rem; opacity: 0; transform: translateX(100%); transition: all 0.3s ease; }
37
+ .toast.show { opacity: 1; transform: translateX(0); }
38
+ .toast.error { border-left-color: #ef4444; }
39
+ .toast.success { border-left-color: #10b981; }
40
+ .toast.warning { border-left-color: #f59e0b; }
41
+ .toast-close { float: right; background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #9ca3af; }
42
+
43
+ /* Loading States & Skeleton */
44
+ .loading-skeleton {
45
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
46
+ background-size: 200% 100%;
47
+ animation: loading 1.5s infinite;
48
+ border-radius: 4px;
49
+ }
50
+ @keyframes loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
51
+
52
+ .skeleton-text { height: 1rem; margin: 0.25rem 0; }
53
+ .skeleton-text.large { height: 1.5rem; }
54
+ .skeleton-text.small { height: 0.75rem; }
55
+ .skeleton-box { height: 100px; border-radius: 8px; }
56
+
57
+ /* Enhanced Loading Spinner */
58
+ .spinner-container { display: flex; align-items: center; justify-content: center; padding: 2rem; }
59
+ .spinner-large { width: 40px; height: 40px; border-width: 4px; }
60
+
61
+ /* Fade in animations */
62
+ .fade-in { animation: fadeIn 0.5s ease-in; }
63
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
64
+
65
+ /* Slide in animations */
66
+ .slide-in { animation: slideIn 0.3s ease-out; }
67
+ @keyframes slideIn { from { transform: translateX(-20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
68
+
69
+ /* Empty states */
70
+ .empty-state {
71
+ text-align: center; padding: 3rem 1rem; color: #6b7280;
72
+ background: #f9fafb; border: 2px dashed #d1d5db; border-radius: 12px; margin: 1rem 0;
73
+ }
74
+ .empty-state-icon { font-size: 3rem; margin-bottom: 1rem; opacity: 0.5; }
75
+ .empty-state-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; color: #374151; }
76
+ .empty-state-description { font-size: 0.875rem; line-height: 1.5; }
77
+
78
+ /* Enhanced Flashcards - Responsive */
79
+ .flashcard-container { perspective: 1000px; position: relative; }
80
+ .flashcard-inner { position: relative; width: 100%; min-height: 280px; transition: transform 0.6s; transform-style: preserve-3d; }
81
+ .flashcard-container.flipped .flashcard-inner { transform: rotateY(180deg); }
82
+ .flashcard-front, .flashcard-back {
83
+ position: absolute; width: 100%; height: 100%; min-height: 280px; backface-visibility: hidden;
84
+ border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; display: flex; flex-direction: column;
85
+ justify-content: space-between; word-wrap: break-word; overflow-wrap: break-word;
86
+ }
87
+ .flashcard-front { background-color: #f9fafb; }
88
+ .flashcard-back { background-color: #ffffff; transform: rotateY(180deg); }
89
+
90
+ /* Responsive text handling */
91
+ .flashcard-text {
92
+ font-size: 1rem; line-height: 1.5; max-height: 150px; overflow-y: auto;
93
+ word-break: break-word; hyphens: auto; padding: 0.25rem 0;
94
+ }
95
+
96
+ /* Mobile responsiveness */
97
+ @media (max-width: 768px) {
98
+ .flashcard-text { font-size: 0.9rem; max-height: 120px; line-height: 1.4; }
99
+ .flashcard-container { margin-bottom: 1rem; }
100
+ .flashcard-inner { min-height: 250px; }
101
+ .flashcard-front, .flashcard-back { min-height: 250px; padding: 16px; }
102
+ .card { padding: 16px; }
103
+ body { padding: 0.5rem; }
104
+ .container { padding: 0; }
105
+
106
+ /* Adjust grid for mobile */
107
+ .grid.grid-cols-1.md\\:grid-cols-3 { grid-template-columns: 1fr; }
108
+ .grid.grid-cols-1.lg\\:grid-cols-2 { grid-template-columns: 1fr; }
109
+ }
110
+
111
+ @media (max-width: 480px) {
112
+ .flashcard-text { font-size: 0.85rem; max-height: 100px; }
113
+ .flashcard-inner { min-height: 220px; }
114
+ .flashcard-front, .flashcard-back { min-height: 220px; padding: 12px; }
115
+
116
+ /* Make buttons more touch-friendly */
117
+ .btn { padding: 0.75rem 1rem; min-height: 44px; }
118
+ .audio-btn { font-size: 1.25rem; padding: 0.5rem; }
119
+ .delete-btn { font-size: 1rem; padding: 0.5rem; }
120
+ }
121
+
122
+ /* Text overflow handling */
123
+ .text-truncate {
124
+ overflow: hidden;
125
+ text-overflow: ellipsis;
126
+ white-space: nowrap;
127
+ }
128
+
129
+ .text-wrap {
130
+ white-space: normal;
131
+ word-wrap: break-word;
132
+ overflow-wrap: break-word;
133
+ }
134
+
135
+ /* Form Validation */
136
+ .form-error { border-color: #ef4444 !important; }
137
+ .error-message { color: #ef4444; font-size: 0.875rem; margin-top: 0.25rem; }
138
+
139
+ /* Enhanced buttons */
140
+ .btn { position: relative; overflow: hidden; }
141
+ .btn::after { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; border-radius: 50%; background: rgba(255,255,255,0.3); transform: translate(-50%, -50%); transition: width 0.3s, height 0.3s; }
142
+ .btn:active::after { width: 300px; height: 300px; }
143
+
144
+ /* Pulse animation for mic button */
145
+ .mic-button.is-recording { animation: pulse 1.5s infinite; }
146
+ @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } }
147
+
148
+ /* Auth Modal */
149
+ .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); }
150
+ .modal.show { display: block; }
151
+ .modal-content {
152
+ background-color: white; margin: 5% auto; padding: 0; width: 90%; max-width: 400px;
153
+ border-radius: 12px; position: relative; animation: modalSlideIn 0.3s ease;
154
+ }
155
+ @keyframes modalSlideIn { from { opacity: 0; transform: translateY(-50px); } to { opacity: 1; transform: translateY(0); } }
156
+ .modal-header { padding: 1.5rem; border-bottom: 1px solid #e5e7eb; }
157
+ .modal-body { padding: 1.5rem; }
158
+ .modal-footer { padding: 1rem 1.5rem; border-top: 1px solid #e5e7eb; display: flex; justify-content: flex-end; gap: 0.5rem; }
159
+ .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
160
+ .close:hover { color: #000; }
161
+
162
+ /* Auth Form */
163
+ .auth-form { display: none; }
164
+ .auth-form.active { display: block; }
165
+ .form-group { margin-bottom: 1rem; }
166
+ .form-group label { display: block; margin-bottom: 0.25rem; font-weight: 500; color: #374151; }
167
+ .form-group input { width: 100%; padding: 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.875rem; }
168
+ .form-group input:focus { outline: none; border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); }
169
+
170
+ /* User Menu */
171
+ .user-menu { position: relative; display: inline-block; }
172
+ .user-menu-content { display: none; position: absolute; right: 0; background-color: white; min-width: 200px; box-shadow: 0 8px 16px rgba(0,0,0,0.1); border-radius: 8px; z-index: 1; border: 1px solid #e5e7eb; }
173
+ .user-menu.show .user-menu-content { display: block; }
174
+ .user-menu-item { padding: 0.75rem 1rem; display: block; color: #374151; text-decoration: none; border-bottom: 1px solid #f3f4f6; }
175
+ .user-menu-item:hover { background-color: #f9fafb; }
176
+ .user-menu-item:last-child { border-bottom: none; }
177
+
178
+ /* Header Auth Section */
179
+ .auth-section { display: flex; align-items: center; gap: 0.5rem; }
180
+ .user-avatar { width: 40px; height: 40px; border-radius: 50%; background-color: #4f46e5; color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; cursor: pointer; }
181
+
182
+ /* Content Curation Styles */
183
+ .content-item {
184
+ border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; margin-bottom: 1rem;
185
+ background: white; transition: all 0.2s;
186
+ }
187
+ .content-item:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); border-color: #4f46e5; }
188
+ .content-item-title { font-weight: 600; color: #1f2937; margin-bottom: 0.5rem; }
189
+ .content-item-meta { font-size: 0.75rem; color: #6b7280; margin-bottom: 0.5rem; }
190
+ .content-item-summary { font-size: 0.875rem; color: #4b5563; line-height: 1.5; }
191
+ .difficulty-badge {
192
+ display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 500;
193
+ }
194
+ .difficulty-easy { background-color: #dcfce7; color: #166534; }
195
+ .difficulty-medium { background-color: #fef3c7; color: #92400e; }
196
+ .difficulty-hard { background-color: #fee2e2; color: #991b1b; }
197
+
198
+ /* Analytics Styles */
199
+ .metric-card {
200
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
201
+ color: white; border-radius: 12px; padding: 1.5rem; text-align: center;
202
+ }
203
+ .metric-value { font-size: 2rem; font-weight: bold; margin-bottom: 0.5rem; }
204
+ .metric-label { font-size: 0.875rem; opacity: 0.9; }
205
+
206
+ /* Study Plan Styles */
207
+ .plan-activity {
208
+ display: flex; align-items: center; padding: 0.75rem; border-radius: 6px;
209
+ border-left: 4px solid #4f46e5; background-color: #f8fafc; margin-bottom: 0.5rem;
210
+ }
211
+ .plan-activity-icon { margin-right: 0.75rem; font-size: 1.25rem; }
212
+ .plan-activity-text { flex-grow: 1; font-size: 0.875rem; }
213
+ .plan-activity-time { font-size: 0.75rem; color: #6b7280; }
214
+ </style>
215
+ </head>
216
+ <body class="p-4 md:p-8">
217
+ <!-- Toast Container -->
218
+ <div id="toastContainer" class="toast-container"></div>
219
+
220
+ <div class="container mx-auto max-w-5xl">
221
+ <header class="mb-10">
222
+ <div class="flex justify-between items-center mb-6">
223
+ <div>
224
+ <h1 class="text-3xl lg:text-4xl font-bold text-gray-800">Dynamic English Study Studio</h1>
225
+ <p class="text-gray-500 mt-2">Your study tool with audio, images, and conversation & pronunciation practice.</p>
226
+ </div>
227
+ <div class="auth-section">
228
+ <!-- Guest state -->
229
+ <div id="guestAuth" class="flex gap-2">
230
+ <button onclick="showAuthModal('login')" class="btn btn-primary">Login</button>
231
+ <button onclick="showAuthModal('register')" class="btn bg-gray-500 hover:bg-gray-600 text-white">Sign Up</button>
232
+ </div>
233
+
234
+ <!-- Authenticated state -->
235
+ <div id="userAuth" class="user-menu" style="display: none;">
236
+ <div class="user-avatar" onclick="toggleUserMenu()" id="userAvatar">U</div>
237
+ <div class="user-menu-content">
238
+ <div class="user-menu-item" style="font-weight: 600; color: #4f46e5;" id="userEmail">user@example.com</div>
239
+ <a href="#" onclick="showUserSettings()" class="user-menu-item">βš™οΈ Settings</a>
240
+ <a href="#" onclick="showUserFlashcards()" class="user-menu-item">πŸ“š My Flashcards</a>
241
+ <a href="#" onclick="logout()" class="user-menu-item">πŸšͺ Logout</a>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </header>
247
+
248
+ <div class="card mb-8">
249
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">Global Session Settings</h2>
250
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
251
+ <div>
252
+ <label for="modelSelector" class="block text-sm font-medium text-gray-700">Main AI Model</label>
253
+ <select id="modelSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
254
+ <option>Loading models...</option>
255
+ </select>
256
+ </div>
257
+ <div>
258
+ <label for="contextSelector" class="block text-sm font-medium text-gray-700">Vocabulary Focus</label>
259
+ <select id="contextSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
260
+ <option value="General/Social">General/Social</option>
261
+ <option value="Professional/Business">Professional/Business</option>
262
+ <option value="Technical/IT">Technical/IT</option>
263
+ </select>
264
+ </div>
265
+ <div>
266
+ <label for="voiceSelector" class="block text-sm font-medium text-gray-700">Voice Accent (TTS)</label>
267
+ <select id="voiceSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
268
+ <option value="co.uk">British (Female)</option>
269
+ <option value="com">American (Female)</option>
270
+ <option value="com.au">Australian (Female)</option>
271
+ </select>
272
+ </div>
273
+ </div>
274
+ </div>
275
+
276
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
277
+ <!-- Coluna da Esquerda: Inputs -->
278
+ <div class="flex flex-col gap-6">
279
+ <div class="card">
280
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">1. Study from Text</h2>
281
+ <div id="textEditor" contenteditable="true" spellcheck="true" lang="en" class="w-full p-4 border rounded-md min-h-[150px]"></div>
282
+ <p class="text-sm text-right text-gray-500 mt-2">Characters: <span id="charCount">0</span>/10000</p>
283
+ <div class="mt-4 border-t pt-4 flex flex-wrap gap-2 justify-between items-center">
284
+ <button id="createCardButton" onclick="createFlashcardFromSelection()" class="btn btn-primary bg-green-600 hover:bg-green-700 disabled:opacity-50" disabled>+ Create Flashcard</button>
285
+ <button id="playButton" onclick="handlePlay()" class="btn btn-primary" disabled>
286
+ <span id="playText">Listen to Text</span>
287
+ <div id="playSpinner" class="spinner ml-2 hidden"></div>
288
+ </button>
289
+ </div>
290
+ <audio id="audioPlayer" controls class="w-full mt-4 hidden"></audio>
291
+ </div>
292
+
293
+ <div class="card">
294
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">2. Study from Image (Analysis)</h2>
295
+ <input type="file" id="imageUploader" accept="image/*" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:font-semibold file:bg-violet-50 file:text-violet-700 hover:file:bg-violet-100"/>
296
+ <img id="imagePreview" src="" class="mt-4 rounded-lg hidden max-h-60 mx-auto" alt="Preview"/>
297
+ <div id="imageAnalysisResult" class="mt-4"></div>
298
+ </div>
299
+
300
+ <div class="card">
301
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">3. Conversation Practice</h2>
302
+ <div id="chatDisplay" class="flex flex-col space-y-2 mb-4"></div>
303
+ <div class="flex gap-2">
304
+ <input type="text" id="chatInput" class="flex-grow border rounded-lg px-3 py-2" placeholder="Type or use the microphone...">
305
+ <button onclick="handleTextInput()" class="btn btn-primary bg-gray-500 hover:bg-gray-600">Send</button>
306
+ <button id="micButton" onclick="handleVoiceInput()" class="btn btn-primary mic-button w-12 h-12 rounded-full">πŸŽ™οΈ</button>
307
+ </div>
308
+ </div>
309
+
310
+ <div class="card">
311
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">4. Image Generator (Nano Banana)</h2>
312
+ <p class="text-sm text-gray-600 mb-2">Describe a scene in English for the AI to draw.</p>
313
+ <div class="flex gap-2">
314
+ <input type="text" id="imagePromptInput" class="flex-grow border rounded-lg px-3 py-2" placeholder="Ex: a blue cat reading a book on the moon">
315
+ <button id="generateImageBtn" onclick="generateImage()" class="btn btn-primary">Generate</button>
316
+ </div>
317
+ <div id="imageResultContainer" class="mt-4 p-4 border rounded-lg bg-gray-50 min-h-[200px] flex items-center justify-center">
318
+ <p class="text-gray-500 italic">Your image will appear here.</p>
319
+ </div>
320
+ </div>
321
+
322
+ <div class="card">
323
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">5. Content Discovery & Curation</h2>
324
+ <div id="contentCuration" style="display: none;">
325
+ <div class="mb-4">
326
+ <h3 class="text-lg font-medium mb-2">πŸ” Find Relevant Content</h3>
327
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
328
+ <div>
329
+ <label class="block text-sm font-medium text-gray-700 mb-1">Search Topic</label>
330
+ <input type="text" id="contentSearchInput" class="w-full border rounded-lg px-3 py-2" placeholder="e.g., cybersecurity, product management">
331
+ </div>
332
+ <div>
333
+ <label class="block text-sm font-medium text-gray-700 mb-1">Category</label>
334
+ <select id="contentCategorySelect" class="w-full border rounded-lg px-3 py-2">
335
+ <option value="">All Categories</option>
336
+ <option value="technology">Technology</option>
337
+ <option value="business">Business</option>
338
+ <option value="automotive">Automotive</option>
339
+ <option value="cybersecurity">Cybersecurity</option>
340
+ <option value="science">Science</option>
341
+ </select>
342
+ </div>
343
+ </div>
344
+ <div class="flex gap-2 mb-4">
345
+ <button onclick="searchContent()" class="btn btn-primary">πŸ” Search Articles</button>
346
+ <button onclick="getRecommendations()" class="btn bg-purple-500 hover:bg-purple-600 text-white">✨ AI Recommendations</button>
347
+ <button onclick="showSavedArticles()" class="btn bg-green-500 hover:bg-green-600 text-white">πŸ“š Saved Articles</button>
348
+ </div>
349
+ </div>
350
+
351
+ <div id="contentResults" class="space-y-4">
352
+ <div class="empty-state">
353
+ <div class="empty-state-icon">πŸ”</div>
354
+ <div class="empty-state-title">Discover Personalized Content</div>
355
+ <div class="empty-state-description">Search for articles or get AI recommendations based on your interests and English level</div>
356
+ </div>
357
+ </div>
358
+
359
+ <div class="mt-6 pt-4 border-t">
360
+ <h3 class="text-lg font-medium mb-2">πŸ“ Add Your Own Content</h3>
361
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
362
+ <input type="text" id="customContentTitle" class="border rounded-lg px-3 py-2" placeholder="Article Title">
363
+ <input type="url" id="customContentUrl" class="border rounded-lg px-3 py-2" placeholder="URL (optional)">
364
+ </div>
365
+ <textarea id="customContentText" rows="4" class="w-full border rounded-lg px-3 py-2 mb-4" placeholder="Paste your text content here..."></textarea>
366
+ <button onclick="saveCustomContent()" class="btn btn-primary">πŸ’Ύ Save Content</button>
367
+ </div>
368
+ </div>
369
+ <div id="contentCurationLogin" class="text-center py-8">
370
+ <div class="empty-state">
371
+ <div class="empty-state-icon">πŸ”</div>
372
+ <div class="empty-state-title">Login Required</div>
373
+ <div class="empty-state-description">
374
+ Please log in to access personalized content curation, save articles, and get AI-powered recommendations.
375
+ </div>
376
+ <button onclick="showAuthModal('login')" class="btn btn-primary mt-4">Login to Continue</button>
377
+ </div>
378
+ </div>
379
+ </div>
380
+
381
+ <div class="card">
382
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">6. Interactive Writing Practice</h2>
383
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
384
+ <div>
385
+ <label for="activityTypeSelector" class="block text-sm font-medium text-gray-700">Activity Type</label>
386
+ <select id="activityTypeSelector" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 rounded-md shadow-sm">
387
+ <option value="translate_pt_en">Translate (PT to EN)</option>
388
+ <option value="email_writing">Write Formal Email</option>
389
+ <option value="summary_writing">Summarize Main Text</option>
390
+ <option value="professional_conversation">Simulate Professional Conversation</option>
391
+ </select>
392
+ </div>
393
+ <div class="flex items-end">
394
+ <button id="generateActivityButton" onclick="generateActivity()" class="btn btn-primary w-full">Generate Activity</button>
395
+ </div>
396
+ </div>
397
+
398
+ <div id="activityChatContainer" class="mt-4 border-t pt-4">
399
+ <div id="activityChatDisplay">
400
+ <p class="text-gray-500 italic">Your interactive exercise will appear here.</p>
401
+ </div>
402
+ <div id="activityResponseArea" class="mt-4 hidden">
403
+ <label for="activityInput" class="block text-sm font-medium text-gray-700 mb-1">Your Answer:</label>
404
+ <textarea id="activityInput" rows="4" class="w-full p-2 border rounded-md"></textarea>
405
+ <button id="submitActivityBtn" onclick="submitActivityResponse()" class="btn btn-primary mt-2">Submit Answer</button>
406
+ </div>
407
+ </div>
408
+ </div>
409
+ </div>
410
+
411
+ <!-- Coluna da Direita: Painel DinΓ’mico -->
412
+ <div class="flex flex-col gap-6">
413
+ <!-- Study Analytics Panel (for logged users) -->
414
+ <div id="analyticsPanel" class="card" style="display: none;">
415
+ <div class="flex justify-between items-center mb-4">
416
+ <h2 class="text-xl font-semibold text-gray-700">πŸ“Š Your Progress</h2>
417
+ <button onclick="toggleAnalytics()" class="text-gray-500 hover:text-gray-700">↕️</button>
418
+ </div>
419
+ <div id="analyticsContent">
420
+ <div class="grid grid-cols-2 gap-4 mb-4">
421
+ <div class="text-center p-4 bg-blue-50 rounded-lg">
422
+ <div class="text-2xl font-bold text-blue-600" id="totalFlashcards">0</div>
423
+ <div class="text-sm text-gray-600">Flashcards</div>
424
+ </div>
425
+ <div class="text-center p-4 bg-green-50 rounded-lg">
426
+ <div class="text-2xl font-bold text-green-600" id="totalArticles">0</div>
427
+ <div class="text-sm text-gray-600">Articles</div>
428
+ </div>
429
+ </div>
430
+ <div class="text-center p-3 bg-purple-50 rounded-lg">
431
+ <div class="text-lg font-semibold text-purple-600" id="currentLevel">B1</div>
432
+ <div class="text-sm text-gray-600">Current Level</div>
433
+ </div>
434
+ </div>
435
+ </div>
436
+
437
+ <!-- Study Planner Panel (for logged users) -->
438
+ <div id="studyPlannerPanel" class="card" style="display: none;">
439
+ <h2 class="text-xl font-semibold mb-4 text-gray-700">🎯 Study Planner</h2>
440
+ <div id="studyPlanContent">
441
+ <div class="mb-4">
442
+ <h3 class="font-medium mb-2">Quick Plan Generator</h3>
443
+ <div class="grid grid-cols-1 gap-2 mb-3">
444
+ <select id="targetLevelSelect" class="border rounded-lg px-3 py-2 text-sm">
445
+ <option value="A2">Target: A2 (Elementary)</option>
446
+ <option value="B1">Target: B1 (Intermediate)</option>
447
+ <option value="B2" selected>Target: B2 (Upper-Intermediate)</option>
448
+ <option value="C1">Target: C1 (Advanced)</option>
449
+ <option value="C2">Target: C2 (Proficient)</option>
450
+ </select>
451
+ <select id="weeklyHoursSelect" class="border rounded-lg px-3 py-2 text-sm">
452
+ <option value="3">3 hours/week</option>
453
+ <option value="5" selected>5 hours/week</option>
454
+ <option value="7">7 hours/week</option>
455
+ <option value="10">10 hours/week</option>
456
+ </select>
457
+ </div>
458
+ <button onclick="generateStudyPlan()" class="btn btn-primary w-full text-sm">πŸš€ Generate Plan</button>
459
+ </div>
460
+ <div id="studyPlanResult" class="text-sm text-gray-600">
461
+ Your personalized study plan will appear here
462
+ </div>
463
+ </div>
464
+ </div>
465
+
466
+ <!-- Flashcards Panel -->
467
+ <div id="flashcardsPanel" class="card">
468
+ <div class="flex justify-between items-center mb-4">
469
+ <h2 class="text-xl font-semibold text-gray-700">Your Study Session</h2>
470
+ <div class="flex items-center gap-2">
471
+ <button id="loadFlashcardsBtn" onclick="showUserFlashcards()" class="btn bg-blue-100 text-blue-700 hover:bg-blue-200 text-sm" style="display: none;" title="Load your saved flashcards">πŸ“š Load Saved</button>
472
+ <div id="sessionStatus" class="text-sm">
473
+ <span id="guestStatus" class="text-gray-500">Guest Mode - Progress not saved</span>
474
+ <span id="userStatus" class="text-green-600" style="display: none;">βœ“ Progress being saved</span>
475
+ </div>
476
+ </div>
477
+ </div>
478
+ <div id="flashcardList" class="space-y-6">
479
+ <div id="noCardsMessage" class="empty-state">
480
+ <div class="empty-state-icon">πŸ“š</div>
481
+ <div class="empty-state-title">No flashcards yet</div>
482
+ <div class="empty-state-description">
483
+ Select text from the editor or analyze an image to create your first flashcard!
484
+ </div>
485
+ </div>
486
+ </div>
487
+ </div>
488
+ </div>
489
+ </div>
490
+ </div>
491
+
492
+ <!-- Auth Modal -->
493
+ <div id="authModal" class="modal">
494
+ <div class="modal-content">
495
+ <div class="modal-header">
496
+ <h2 id="authModalTitle">Login</h2>
497
+ <span class="close" onclick="hideAuthModal()">&times;</span>
498
+ </div>
499
+ <div class="modal-body">
500
+ <!-- Login Form -->
501
+ <form id="loginForm" class="auth-form active">
502
+ <div class="form-group">
503
+ <label for="loginEmail">Email</label>
504
+ <input type="email" id="loginEmail" required>
505
+ </div>
506
+ <div class="form-group">
507
+ <label for="loginPassword">Password</label>
508
+ <input type="password" id="loginPassword" required>
509
+ </div>
510
+ <button type="submit" class="btn btn-primary w-full">Login</button>
511
+ <p class="text-center mt-4 text-sm text-gray-600">
512
+ Don't have an account?
513
+ <a href="#" onclick="switchAuthForm('register')" class="text-indigo-600 hover:text-indigo-800">Sign up</a>
514
+ </p>
515
+ </form>
516
+
517
+ <!-- Register Form -->
518
+ <form id="registerForm" class="auth-form">
519
+ <div class="form-group">
520
+ <label for="registerEmail">Email</label>
521
+ <input type="email" id="registerEmail" required>
522
+ </div>
523
+ <div class="form-group">
524
+ <label for="registerPassword">Password</label>
525
+ <input type="password" id="registerPassword" required minlength="8">
526
+ <small class="text-gray-500">At least 8 characters</small>
527
+ </div>
528
+ <div class="form-group">
529
+ <label for="confirmPassword">Confirm Password</label>
530
+ <input type="password" id="confirmPassword" required>
531
+ </div>
532
+ <button type="submit" class="btn btn-primary w-full">Create Account</button>
533
+ <p class="text-center mt-4 text-sm text-gray-600">
534
+ Already have an account?
535
+ <a href="#" onclick="switchAuthForm('login')" class="text-indigo-600 hover:text-indigo-800">Login</a>
536
+ </p>
537
+ </form>
538
+ </div>
539
+ </div>
540
+ </div>
541
+
542
+ <script>
543
+ // --- ELEMENTOS GLOBAIS ---
544
+ const modelSelector = document.getElementById('modelSelector');
545
+ const contextSelector = document.getElementById('contextSelector');
546
+ const voiceSelector = document.getElementById('voiceSelector');
547
+ const textEditor = document.getElementById('textEditor');
548
+ const createCardButton = document.getElementById('createCardButton');
549
+ const playButton = document.getElementById('playButton');
550
+ const audioPlayer = document.getElementById('audioPlayer');
551
+ const flashcardList = document.getElementById('flashcardList');
552
+ const noCardsMessage = document.getElementById('noCardsMessage');
553
+ const imageUploader = document.getElementById('imageUploader');
554
+ const imagePreview = document.getElementById('imagePreview');
555
+ const imageAnalysisResult = document.getElementById('imageAnalysisResult');
556
+ const chatDisplay = document.getElementById('chatDisplay');
557
+ const chatInput = document.getElementById('chatInput');
558
+ const micButton = document.getElementById('micButton');
559
+ const imagePromptInput = document.getElementById('imagePromptInput');
560
+ const generateImageBtn = document.getElementById('generateImageBtn');
561
+ const imageResultContainer = document.getElementById('imageResultContainer');
562
+ const activityTypeSelector = document.getElementById('activityTypeSelector');
563
+ const generateActivityButton = document.getElementById('generateActivityButton');
564
+ const activityChatDisplay = document.getElementById('activityChatDisplay');
565
+ const activityResponseArea = document.getElementById('activityResponseArea');
566
+ const activityInput = document.getElementById('activityInput');
567
+ const submitActivityBtn = document.getElementById('submitActivityBtn');
568
+ const toastContainer = document.getElementById('toastContainer');
569
+
570
+ let chatHistory = [];
571
+ let recognition;
572
+ let isRecording = false;
573
+ let currentActivityPrompt = "";
574
+ let currentUser = null;
575
+
576
+ // --- SISTEMA DE TOAST NOTIFICATIONS ---
577
+ function showToast(message, type = 'info', duration = 5000) {
578
+ const toast = document.createElement('div');
579
+ toast.className = `toast ${type}`;
580
+ toast.innerHTML = `
581
+ <button class="toast-close" onclick="this.parentElement.remove()">&times;</button>
582
+ <div class="font-medium">${getToastTitle(type)}</div>
583
+ <div class="text-sm mt-1">${message}</div>
584
+ `;
585
+
586
+ toastContainer.appendChild(toast);
587
+
588
+ // Show animation
589
+ setTimeout(() => toast.classList.add('show'), 100);
590
+
591
+ // Auto remove
592
+ setTimeout(() => {
593
+ toast.classList.remove('show');
594
+ setTimeout(() => toast.remove(), 300);
595
+ }, duration);
596
+ }
597
+
598
+ function getToastTitle(type) {
599
+ const titles = {
600
+ 'success': 'βœ… Success',
601
+ 'error': '❌ Error',
602
+ 'warning': '⚠️ Warning',
603
+ 'info': 'ℹ️ Info'
604
+ };
605
+ return titles[type] || 'Info';
606
+ }
607
+
608
+ // --- ENHANCED ERROR HANDLING ---
609
+ async function handleApiRequest(url, options, errorContext = 'Request') {
610
+ try {
611
+ const response = await fetch(url, options);
612
+
613
+ if (!response.ok) {
614
+ let errorMessage = `HTTP ${response.status}`;
615
+ try {
616
+ const errorData = await response.json();
617
+ errorMessage = errorData.error || errorMessage;
618
+ } catch (e) {
619
+ // Use default HTTP error
620
+ }
621
+ throw new Error(errorMessage);
622
+ }
623
+
624
+ return await response.json();
625
+ } catch (error) {
626
+ console.error(`${errorContext} failed:`, error);
627
+
628
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
629
+ showToast('Network connection error. Please check your internet connection.', 'error');
630
+ } else if (error.message.includes('503')) {
631
+ showToast('Service temporarily unavailable. Please check API configuration.', 'warning');
632
+ } else {
633
+ showToast(`${errorContext} failed: ${error.message}`, 'error');
634
+ }
635
+
636
+ throw error;
637
+ }
638
+ }
639
+
640
+ // --- LOADING STATE MANAGEMENT ---
641
+ function setButtonLoading(button, loading, originalText = null) {
642
+ if (loading) {
643
+ button.disabled = true;
644
+ button.dataset.originalText = button.innerHTML;
645
+ button.innerHTML = '<div class="spinner mr-2"></div> Loading...';
646
+ } else {
647
+ button.disabled = false;
648
+ button.innerHTML = originalText || button.dataset.originalText || button.innerHTML;
649
+ }
650
+ }
651
+
652
+ // --- INPUT VALIDATION ---
653
+ function validateInput(input, errorMessage) {
654
+ const value = input.value?.trim() || '';
655
+ const errorElement = input.parentNode.querySelector('.error-message');
656
+
657
+ if (!value) {
658
+ input.classList.add('form-error');
659
+ if (!errorElement) {
660
+ const error = document.createElement('div');
661
+ error.className = 'error-message';
662
+ error.textContent = errorMessage;
663
+ input.parentNode.appendChild(error);
664
+ }
665
+ return false;
666
+ } else {
667
+ input.classList.remove('form-error');
668
+ if (errorElement) errorElement.remove();
669
+ return true;
670
+ }
671
+ }
672
+
673
+ // --- CARREGAMENTO DINΓ‚MICO DE MODELOS ---
674
+ async function populateModelSelector() {
675
+ try {
676
+ const models = await handleApiRequest('/list-models', {}, 'Loading models');
677
+ modelSelector.innerHTML = '';
678
+
679
+ const geminiGroup = document.createElement('optgroup');
680
+ geminiGroup.label = 'Google Gemini';
681
+ const groqGroup = document.createElement('optgroup');
682
+ groqGroup.label = 'Groq (Ultra Fast)';
683
+
684
+ models.forEach(model => {
685
+ const option = document.createElement('option');
686
+ option.value = model.value;
687
+ option.textContent = model.name;
688
+ if (model.value.startsWith('gemini:')) {
689
+ geminiGroup.appendChild(option);
690
+ } else if (model.value.startsWith('groq:')) {
691
+ groqGroup.appendChild(option);
692
+ }
693
+ });
694
+
695
+ modelSelector.appendChild(geminiGroup);
696
+ modelSelector.appendChild(groqGroup);
697
+ showToast('Models loaded successfully', 'success', 2000);
698
+ } catch (error) {
699
+ modelSelector.innerHTML = '<option>Error loading models</option>';
700
+ }
701
+ }
702
+
703
+ // --- FUNÇÕES DE ÁUDIO ---
704
+ async function fetchAudioBlob(text) {
705
+ const selectedTld = voiceSelector.value;
706
+ const response = await fetch('/tts-proxy', {
707
+ method: 'POST',
708
+ headers: { 'Content-Type': 'application/json' },
709
+ body: JSON.stringify({ text: text, tld: selectedTld })
710
+ });
711
+ if (!response.ok) {
712
+ const err = await response.json();
713
+ throw new Error(err.error || `HTTP error! status: ${response.status}`);
714
+ }
715
+ return await response.blob();
716
+ }
717
+
718
+ async function playAudio(text, event) {
719
+ if(event) event.stopPropagation();
720
+ if (!text || !text.trim()) {
721
+ showToast('No text to play', 'warning', 2000);
722
+ return;
723
+ }
724
+
725
+ try {
726
+ const audioBlob = await fetchAudioBlob(text);
727
+ const audioUrl = URL.createObjectURL(audioBlob);
728
+ const audio = new Audio(audioUrl);
729
+
730
+ audio.onplay = () => showToast('Playing audio...', 'info', 1000);
731
+ audio.onended = () => URL.revokeObjectURL(audioUrl);
732
+ audio.onerror = () => showToast('Failed to play audio', 'error');
733
+
734
+ await audio.play();
735
+ } catch (error) {
736
+ console.error('Error playing audio:', error);
737
+ showToast('Failed to generate audio. Please try again.', 'error');
738
+ }
739
+ }
740
+
741
+ // --- LΓ“GICA DO EDITOR ---
742
+ const charCount = document.getElementById('charCount');
743
+ const playText = document.getElementById('playText');
744
+ const playSpinner = document.getElementById('playSpinner');
745
+ textEditor.addEventListener('input', () => {
746
+ const len = textEditor.innerText.length;
747
+ charCount.textContent = `${len}/10000`;
748
+ playButton.disabled = len === 0;
749
+ });
750
+ textEditor.addEventListener('mouseup', () => {
751
+ createCardButton.disabled = !window.getSelection().toString().trim();
752
+ });
753
+ async function handlePlay() {
754
+ const text = textEditor.innerText.trim();
755
+ if (!text) {
756
+ showToast('Please enter some text first', 'warning');
757
+ return;
758
+ }
759
+
760
+ if (text.length > 5000) {
761
+ showToast('Text is too long for audio generation. Please use shorter text.', 'warning');
762
+ return;
763
+ }
764
+
765
+ setButtonLoading(playButton, true);
766
+ playText.textContent = 'Generating...';
767
+ playSpinner.classList.remove('hidden');
768
+
769
+ try {
770
+ const audioBlob = await fetchAudioBlob(text);
771
+ const audioUrl = URL.createObjectURL(audioBlob);
772
+ audioPlayer.src = audioUrl;
773
+ audioPlayer.classList.remove('hidden');
774
+
775
+ audioPlayer.onplay = () => showToast('Audio ready! Playing...', 'success', 1000);
776
+ audioPlayer.onended = () => URL.revokeObjectURL(audioUrl);
777
+ audioPlayer.onerror = () => showToast('Failed to play audio', 'error');
778
+
779
+ await audioPlayer.play();
780
+ } catch (error) {
781
+ showToast('Failed to generate audio. Please check your connection and try again.', 'error');
782
+ } finally {
783
+ setButtonLoading(playButton, false);
784
+ playText.textContent = 'Listen to Text';
785
+ playSpinner.classList.add('hidden');
786
+ }
787
+ }
788
+
789
+ // --- TODAS AS OUTRAS FUNÇÕES ---
790
+
791
+ // ATIVIDADES
792
+ async function generateActivity() {
793
+ const activityType = activityTypeSelector.value;
794
+ const context = contextSelector.value;
795
+ const sourceText = textEditor.innerText;
796
+ generateActivityButton.disabled = true;
797
+ generateActivityButton.innerHTML = '<div class="spinner mr-2"></div> Generating...';
798
+ activityChatDisplay.innerHTML = '<p class="text-gray-500 italic">Generating your exercise...</p>';
799
+ activityResponseArea.classList.add('hidden');
800
+ let prompt = "";
801
+ if (activityType === 'translate_pt_en') {
802
+ prompt = `Based on the '${context}' context, create one single sentence in Portuguese for me to translate into English.`;
803
+ } else if (activityType === 'email_writing') {
804
+ prompt = `Create a scenario for me to write a formal email, related to the '${context}' context. Give me clear instructions on what to write.`;
805
+ } else if (activityType === 'summary_writing') {
806
+ if (!sourceText) {
807
+ alert("Please paste text in the main editor to generate a summary activity.");
808
+ activityChatDisplay.innerHTML = '<p class="text-red-500">Please paste text in the editor in card #1 first.</p>';
809
+ generateActivityButton.disabled = false;
810
+ generateActivityButton.textContent = 'Generate Activity';
811
+ return;
812
+ }
813
+ prompt = `Your task is to summarize the following text in 2 or 3 sentences: "${sourceText}"`;
814
+ } else if (activityType === 'professional_conversation') {
815
+ prompt = `Start a professional role-playing conversation with me related to '${context}'. Ask me an open-ended question to begin.`;
816
+ }
817
+ try {
818
+ const response = await fetch('/explain-proxy', {
819
+ method: 'POST',
820
+ headers: { 'Content-Type': 'application/json' },
821
+ body: JSON.stringify({
822
+ model: modelSelector.value,
823
+ context_focus: context,
824
+ custom_prompt: prompt
825
+ })
826
+ });
827
+ if (!response.ok) throw new Error((await response.json()).error);
828
+ const data = await response.json();
829
+ currentActivityPrompt = data.explanation;
830
+ activityChatDisplay.innerHTML = `<div class="ai-prompt"><strong>Your Task:</strong><p>${currentActivityPrompt}</p></div>`;
831
+ activityResponseArea.classList.remove('hidden');
832
+ } catch (error) {
833
+ activityChatDisplay.innerHTML = `<p class="text-red-600 font-semibold">Failed to generate activity: ${error.message}</p>`;
834
+ } finally {
835
+ generateActivityButton.disabled = false;
836
+ generateActivityButton.textContent = 'Generate Activity';
837
+ }
838
+ }
839
+ async function submitActivityResponse() {
840
+ const userResponse = activityInput.value.trim();
841
+ if (!userResponse) {
842
+ alert("Please write a response.");
843
+ return;
844
+ }
845
+ submitActivityBtn.disabled = true;
846
+ submitActivityBtn.innerHTML = '<div class="spinner mr-2"></div> Getting Feedback...';
847
+ const oldFeedback = activityChatDisplay.querySelector('.ai-feedback');
848
+ if(oldFeedback) oldFeedback.remove();
849
+ const oldResponse = activityChatDisplay.querySelector('.user-response-display');
850
+ if(oldResponse) oldResponse.remove();
851
+ const userResponseDiv = document.createElement('div');
852
+ userResponseDiv.className = 'user-response-display';
853
+ userResponseDiv.innerHTML = `<strong>Your Response:</strong><p>${userResponse}</p>`;
854
+ activityChatDisplay.appendChild(userResponseDiv);
855
+ try {
856
+ const response = await fetch('/activity-feedback', {
857
+ method: 'POST',
858
+ headers: { 'Content-Type': 'application/json' },
859
+ body: JSON.stringify({
860
+ model: modelSelector.value,
861
+ context_focus: contextSelector.value,
862
+ original_prompt: currentActivityPrompt,
863
+ user_response: userResponse
864
+ })
865
+ });
866
+ if (!response.ok) throw new Error((await response.json()).error);
867
+ const data = await response.json();
868
+ const feedbackDiv = document.createElement('div');
869
+ feedbackDiv.className = 'ai-feedback';
870
+ feedbackDiv.innerHTML = `<strong>Feedback:</strong><div class="prose prose-sm max-w-none">${marked.parse(data.feedback)}</div>`;
871
+ activityChatDisplay.appendChild(feedbackDiv);
872
+ activityInput.value = '';
873
+ } catch (error) {
874
+ const errorDiv = document.createElement('div');
875
+ errorDiv.className = 'ai-feedback';
876
+ errorDiv.innerHTML = `<p class="text-red-600 font-semibold">Failed to get feedback: ${error.message}</p>`;
877
+ activityChatDisplay.appendChild(errorDiv);
878
+ } finally {
879
+ submitActivityBtn.disabled = false;
880
+ submitActivityBtn.textContent = 'Submit Answer';
881
+ }
882
+ }
883
+
884
+ // CONVERSAÇÃO
885
+ function initializeSpeechRecognition() {
886
+ window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
887
+ if (!window.SpeechRecognition) {
888
+ micButton.disabled = true;
889
+ micButton.textContent = '❌';
890
+ alert('Voice recognition API is not supported in this browser.');
891
+ return;
892
+ }
893
+ recognition = new SpeechRecognition();
894
+ recognition.lang = 'en-US';
895
+ recognition.interimResults = false;
896
+ recognition.continuous = false;
897
+ recognition.onresult = (event) => {
898
+ const transcript = event.results[0][0].transcript;
899
+ chatInput.value = transcript;
900
+ sendMessage(transcript);
901
+ };
902
+ recognition.onerror = (event) => { console.error('Voice recognition error:', event.error); };
903
+ recognition.onend = () => {
904
+ isRecording = false;
905
+ micButton.classList.remove('is-recording');
906
+ micButton.innerHTML = 'πŸŽ™οΈ';
907
+ };
908
+ }
909
+ function handleVoiceInput() {
910
+ if (isRecording) {
911
+ recognition.stop();
912
+ } else {
913
+ isRecording = true;
914
+ micButton.classList.add('is-recording');
915
+ micButton.innerHTML = '<div class="spinner"></div>';
916
+ recognition.start();
917
+ }
918
+ }
919
+ function handleTextInput() {
920
+ const message = chatInput.value.trim();
921
+ sendMessage(message);
922
+ chatInput.value = '';
923
+ }
924
+ chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleTextInput(); });
925
+ async function sendMessage(message) {
926
+ if (!message) return;
927
+ appendMessage(message, 'user');
928
+ chatHistory.push({ role: 'user', content: message });
929
+ try {
930
+ const response = await fetch('/chat-with-ai', {
931
+ method: 'POST',
932
+ headers: { 'Content-Type': 'application/json' },
933
+ body: JSON.stringify({ history: chatHistory, message: message })
934
+ });
935
+ if (!response.ok) throw new Error((await response.json()).error);
936
+ const data = await response.json();
937
+ appendMessage(data.response, 'ai');
938
+ chatHistory.push({ role: 'assistant', content: data.response });
939
+ playAudio(data.response);
940
+ } catch (error) {
941
+ appendMessage(`Chat error: ${error.message}`, 'ai');
942
+ }
943
+ }
944
+ function appendMessage(text, sender) {
945
+ const msg = document.createElement('div');
946
+ msg.className = `chat-message ${sender}-message`;
947
+ msg.textContent = text;
948
+ chatDisplay.appendChild(msg);
949
+ chatDisplay.scrollTop = chatDisplay.scrollHeight;
950
+ }
951
+
952
+ // IMAGEM
953
+ imageUploader.addEventListener('change', (event) => {
954
+ const file = event.target.files[0];
955
+ if (!file) return;
956
+ const reader = new FileReader();
957
+ reader.onload = (e) => {
958
+ imagePreview.src = e.target.result;
959
+ imagePreview.classList.remove('hidden');
960
+ analyzeImage(e.target.result);
961
+ };
962
+ reader.readAsDataURL(file);
963
+ });
964
+ async function analyzeImage(base64Image) {
965
+ // Show skeleton loading
966
+ imageAnalysisResult.innerHTML = `
967
+ <div class="spinner-container">
968
+ <div class="spinner spinner-large !border-l-indigo-500 !border-gray-200"></div>
969
+ <p class="ml-3 text-gray-600">Analyzing image with AI...</p>
970
+ </div>
971
+ <div class="mt-4">
972
+ <div class="skeleton-text loading-skeleton mb-2"></div>
973
+ <div class="flex gap-2">
974
+ <div class="skeleton-box loading-skeleton" style="width: 80px; height: 32px;"></div>
975
+ <div class="skeleton-box loading-skeleton" style="width: 100px; height: 32px;"></div>
976
+ <div class="skeleton-box loading-skeleton" style="width: 90px; height: 32px;"></div>
977
+ </div>
978
+ </div>`;
979
+
980
+ try {
981
+ const vocabulary = await handleApiRequest('/analyze-image', {
982
+ method: 'POST',
983
+ headers: { 'Content-Type': 'application/json' },
984
+ body: JSON.stringify({
985
+ image: base64Image,
986
+ model: modelSelector.value
987
+ })
988
+ }, 'Analyzing image');
989
+
990
+ displayImageVocabulary(vocabulary);
991
+ showToast(`Found ${vocabulary.length} vocabulary items in the image!`, 'success');
992
+ } catch (error) {
993
+ imageAnalysisResult.innerHTML = `
994
+ <div class="empty-state">
995
+ <div class="empty-state-icon">❌</div>
996
+ <div class="empty-state-title">Analysis Failed</div>
997
+ <div class="empty-state-description">Please try uploading the image again or check your connection.</div>
998
+ </div>`;
999
+ }
1000
+ }
1001
+ function displayImageVocabulary(vocabulary) {
1002
+ imageAnalysisResult.innerHTML = `
1003
+ <div class="fade-in">
1004
+ <h3 class="font-semibold mb-3 text-gray-700">🎯 Suggested Vocabulary (${vocabulary.length} items)</h3>
1005
+ <div class="flex flex-wrap gap-2" id="vocabularyButtons"></div>
1006
+ <p class="text-xs text-gray-500 mt-3">Click any term to create a flashcard</p>
1007
+ </div>`;
1008
+
1009
+ const buttonContainer = document.getElementById('vocabularyButtons');
1010
+ vocabulary.forEach(({ term, definition }, index) => {
1011
+ const btn = document.createElement('button');
1012
+ btn.className = 'btn bg-violet-100 text-violet-700 hover:bg-violet-200 text-sm slide-in';
1013
+ btn.style.animationDelay = `${index * 0.1}s`;
1014
+ btn.innerHTML = `${term} <span class="text-violet-500 ml-1">+</span>`;
1015
+ btn.title = `${term}: ${definition}`;
1016
+ btn.onclick = () => {
1017
+ btn.disabled = true;
1018
+ btn.innerHTML = '<div class="spinner mr-1"></div> Creating...';
1019
+ createFlashcardFromSuggestion(term, definition);
1020
+ };
1021
+ buttonContainer.appendChild(btn);
1022
+ });
1023
+ }
1024
+ async function generateImage() {
1025
+ const prompt = imagePromptInput.value.trim();
1026
+ if (!prompt) {
1027
+ alert('Please enter a description for the image.');
1028
+ return;
1029
+ }
1030
+ generateImageBtn.disabled = true;
1031
+ generateImageBtn.innerHTML = '<div class="spinner mr-2"></div> Generating...';
1032
+ imageResultContainer.innerHTML = `<div class="flex items-center justify-center p-4"><div class="spinner !border-l-indigo-500 !border-gray-200"></div><p class="ml-3 text-gray-600">Creating your image...</p></div>`;
1033
+ try {
1034
+ const response = await fetch('/generate-image', {
1035
+ method: 'POST',
1036
+ headers: { 'Content-Type': 'application/json' },
1037
+ body: JSON.stringify({ prompt: prompt })
1038
+ });
1039
+ if (!response.ok) throw new Error((await response.json()).error);
1040
+ const data = await response.json();
1041
+ imageResultContainer.innerHTML = `<img src="data:image/png;base64,${data.image_base64}" class="rounded-lg max-h-60 mx-auto" alt="AI-generated image"/>`;
1042
+ } catch (error) {
1043
+ imageResultContainer.innerHTML = `<p class="text-red-600 font-semibold">Failed to generate image: ${error.message}</p>`;
1044
+ } finally {
1045
+ generateImageBtn.disabled = false;
1046
+ generateImageBtn.textContent = 'Generate';
1047
+ }
1048
+ }
1049
+
1050
+ // FLASHCARDS
1051
+ function createSkeletonFlashcard() {
1052
+ const cardId = `skeleton-${Date.now()}`;
1053
+ const container = document.createElement('div');
1054
+ container.className = 'flashcard-container fade-in';
1055
+ container.id = cardId;
1056
+ container.innerHTML = `
1057
+ <div class="flashcard-inner">
1058
+ <div class="flashcard-front">
1059
+ <div>
1060
+ <div class="skeleton-text small loading-skeleton"></div>
1061
+ <div class="skeleton-text large loading-skeleton mt-4"></div>
1062
+ <div class="skeleton-text loading-skeleton"></div>
1063
+ </div>
1064
+ <div class="border-t pt-3 mt-3">
1065
+ <div class="skeleton-text loading-skeleton"></div>
1066
+ <div class="skeleton-text small loading-skeleton mt-2"></div>
1067
+ </div>
1068
+ </div>
1069
+ </div>`;
1070
+ return container;
1071
+ }
1072
+
1073
+ async function createFlashcard(word, context, definition = null) {
1074
+ if (!word || !word.trim()) {
1075
+ showToast('Please select a word first', 'warning');
1076
+ return;
1077
+ }
1078
+
1079
+ // Hide empty state and show skeleton
1080
+ noCardsMessage.classList.add('hidden');
1081
+ const skeletonCard = createSkeletonFlashcard();
1082
+ flashcardList.prepend(skeletonCard);
1083
+
1084
+ const btn = definition ? null : createCardButton;
1085
+ if (btn) setButtonLoading(btn, true);
1086
+
1087
+ try {
1088
+ const cardData = await handleApiRequest('/explain-proxy', {
1089
+ method: 'POST',
1090
+ headers: { 'Content-Type': 'application/json' },
1091
+ body: JSON.stringify({
1092
+ word: word.trim(),
1093
+ context: context || 'General context',
1094
+ for_flashcard: true,
1095
+ model: modelSelector.value,
1096
+ context_focus: contextSelector.value
1097
+ })
1098
+ }, 'Creating flashcard');
1099
+
1100
+ if(definition && !cardData.definition) cardData.definition = definition;
1101
+
1102
+ // Remove skeleton and add real card
1103
+ skeletonCard.remove();
1104
+ renderFlashcard(cardData);
1105
+
1106
+ // Show appropriate message based on auth status
1107
+ if (currentUser) {
1108
+ showToast(`Flashcard for "${word}" created and saved to your account!`, 'success');
1109
+ } else {
1110
+ showToast(`Flashcard for "${word}" created! Login to save your progress.`, 'success');
1111
+ }
1112
+ } catch (error) {
1113
+ // Remove skeleton on error
1114
+ skeletonCard.remove();
1115
+ // Show empty state if no cards left
1116
+ if (flashcardList.children.length === 1) {
1117
+ noCardsMessage.classList.remove('hidden');
1118
+ }
1119
+ } finally {
1120
+ if (btn) setButtonLoading(btn, false);
1121
+ }
1122
+ }
1123
+ function createFlashcardFromSelection() {
1124
+ const selection = window.getSelection().toString().trim();
1125
+ if (selection) createFlashcard(selection, textEditor.innerText);
1126
+ }
1127
+ function createFlashcardFromSuggestion(term, definition) {
1128
+ createFlashcard(term, `(From image analysis) A visual representation of ${term}.`, definition);
1129
+ }
1130
+ function renderFlashcard(data) {
1131
+ noCardsMessage.classList.add('hidden');
1132
+ const cardId = `card-${Date.now()}`;
1133
+ const container = document.createElement('div');
1134
+ container.className = 'flashcard-container';
1135
+ container.id = cardId;
1136
+
1137
+ // Calculate dynamic height based on content length
1138
+ const totalLength = (data.gapped_sentence?.length || 0) +
1139
+ (data.translation?.length || 0) +
1140
+ (data.definition?.length || 0) +
1141
+ (data.context_sentence?.length || 0);
1142
+
1143
+ let minHeight = 280;
1144
+ if (totalLength > 300) minHeight = 350;
1145
+ if (totalLength > 500) minHeight = 420;
1146
+ if (totalLength > 800) minHeight = 500;
1147
+
1148
+ // Responsive height adjustment
1149
+ if (window.innerWidth <= 768) {
1150
+ minHeight = Math.max(250, minHeight * 0.85);
1151
+ }
1152
+ if (window.innerWidth <= 480) {
1153
+ minHeight = Math.max(220, minHeight * 0.75);
1154
+ }
1155
+
1156
+ container.style.minHeight = `${minHeight}px`;
1157
+ const fallback = 'Info not generated';
1158
+ container.innerHTML = `
1159
+ <button class="delete-btn" onclick="deleteFlashcard('${cardId}', event)" title="Delete flashcard">πŸ—‘οΈ</button>
1160
+ <div class="flashcard-inner">
1161
+ <div class="flashcard-front">
1162
+ <div>
1163
+ <div class="text-sm text-gray-600 mb-2">Context Sentence:</div>
1164
+ <div class="flashcard-text text-lg text-center text-gray-800">${data.gapped_sentence || fallback}</div>
1165
+ </div>
1166
+ <div class="border-t pt-3 mt-3 text-center">
1167
+ <strong class="text-indigo-600">Hint (PT):</strong>
1168
+ <div class="flashcard-text mt-1">${data.translation || fallback}</div>
1169
+ </div>
1170
+ <div class="text-xs text-center text-gray-400 mt-4">πŸ‘† Click to see the answer</div>
1171
+ </div>
1172
+ <div class="flashcard-back">
1173
+ <div>
1174
+ <h3 class="text-xl font-bold text-center text-indigo-600 mb-2">${data.term || 'Term'}</h3>
1175
+ <div class="flex items-center justify-center gap-2 text-gray-600 italic mb-3">
1176
+ <button class="audio-btn" onclick="playAudio('${(data.context_sentence || '').replace(/'/g, "\\'")}', event)" title="Listen to pronunciation">πŸ”Š</button>
1177
+ <span class="flashcard-text">${data.context_sentence || fallback}</span>
1178
+ </div>
1179
+ <div class="text-sm text-gray-800"><strong class="font-semibold">Definition:</strong>
1180
+ <div class="flashcard-text mt-1">${data.definition || fallback}</div>
1181
+ </div>
1182
+ </div>
1183
+ <div class="border-t mt-3 pt-3">
1184
+ <h4 class="text-sm font-semibold text-center">Practice Your Pronunciation</h4>
1185
+ <div class="flex justify-center items-center gap-2 mt-2">
1186
+ <button class="btn btn-primary w-12 h-12 rounded-full mic-button" onclick="handlePronunciationPractice(this, '${(data.context_sentence || '').replace(/'/g, "\\'")}', event)" title="Record your pronunciation">πŸŽ™οΈ</button>
1187
+ </div>
1188
+ <div class="text-xs text-gray-600 p-2 mt-2 bg-gray-50 rounded-md min-h-[40px] flashcard-text" data-feedback-area>Click the microphone to practice pronunciation</div>
1189
+ </div>
1190
+ </div>
1191
+ </div>`;
1192
+ container.classList.add('fade-in');
1193
+ flashcardList.prepend(container);
1194
+
1195
+ container.addEventListener('click', (e) => {
1196
+ if(e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
1197
+ container.classList.toggle('flipped');
1198
+ }
1199
+ });
1200
+ }
1201
+ function deleteFlashcard(cardId, event) {
1202
+ event.stopPropagation();
1203
+ const cardToDelete = document.getElementById(cardId);
1204
+ if (cardToDelete) {
1205
+ cardToDelete.remove();
1206
+ }
1207
+ if (flashcardList.children.length === 1) {
1208
+ noCardsMessage.classList.remove('hidden');
1209
+ }
1210
+ }
1211
+ async function handlePronunciationPractice(button, targetText, event) {
1212
+ event.stopPropagation();
1213
+ const feedbackArea = button.closest('.flashcard-back').querySelector('[data-feedback-area]');
1214
+ const practiceRecognition = new SpeechRecognition();
1215
+ practiceRecognition.lang = 'en-US';
1216
+ button.innerHTML = '<div class="spinner"></div>';
1217
+ button.disabled = true;
1218
+ feedbackArea.textContent = 'Listening...';
1219
+ practiceRecognition.start();
1220
+ practiceRecognition.onresult = async (e) => {
1221
+ const userText = e.results[0][0].transcript;
1222
+ feedbackArea.textContent = `You said: "${userText}". Analyzing...`;
1223
+ try {
1224
+ const response = await fetch('/pronunciation-feedback', {
1225
+ method: 'POST',
1226
+ headers: { 'Content-Type': 'application/json' },
1227
+ body: JSON.stringify({ target_text: targetText, user_text: userText })
1228
+ });
1229
+ if (!response.ok) throw new Error((await response.json()).error);
1230
+ const data = await response.json();
1231
+ feedbackArea.innerHTML = `<strong>Feedback:</strong> ${data.feedback}`;
1232
+ } catch (error) {
1233
+ feedbackArea.textContent = `Analysis error: ${error.message}`;
1234
+ } finally {
1235
+ button.innerHTML = 'πŸŽ™οΈ';
1236
+ button.disabled = false;
1237
+ }
1238
+ };
1239
+ practiceRecognition.onerror = (e) => {
1240
+ feedbackArea.textContent = `Recording error: ${e.error}`;
1241
+ button.innerHTML = 'πŸŽ™οΈ';
1242
+ button.disabled = false;
1243
+ };
1244
+ }
1245
+
1246
+ // --- AUTHENTICATION FUNCTIONS ---
1247
+
1248
+ async function checkAuthStatus() {
1249
+ try {
1250
+ const response = await fetch('/auth/check');
1251
+ const data = await response.json();
1252
+
1253
+ if (data.authenticated) {
1254
+ currentUser = data.user;
1255
+ showAuthenticatedState();
1256
+ // Load user settings into selectors
1257
+ if (data.user.settings) {
1258
+ loadUserSettings(data.user.settings);
1259
+ }
1260
+ } else {
1261
+ showGuestState();
1262
+ }
1263
+ } catch (error) {
1264
+ console.error('Auth check failed:', error);
1265
+ showGuestState();
1266
+ }
1267
+ }
1268
+
1269
+ function showAuthenticatedState() {
1270
+ document.getElementById('guestAuth').style.display = 'none';
1271
+ document.getElementById('userAuth').style.display = 'block';
1272
+ document.getElementById('userEmail').textContent = currentUser.email;
1273
+ document.getElementById('userAvatar').textContent = currentUser.email.charAt(0).toUpperCase();
1274
+
1275
+ // Update session status
1276
+ document.getElementById('guestStatus').style.display = 'none';
1277
+ document.getElementById('userStatus').style.display = 'inline';
1278
+ document.getElementById('loadFlashcardsBtn').style.display = 'inline-block';
1279
+
1280
+ // Show advanced panels
1281
+ document.getElementById('contentCuration').style.display = 'block';
1282
+ document.getElementById('contentCurationLogin').style.display = 'none';
1283
+ document.getElementById('analyticsPanel').style.display = 'block';
1284
+ document.getElementById('studyPlannerPanel').style.display = 'block';
1285
+
1286
+ // Load analytics and study plan
1287
+ loadAnalytics();
1288
+ loadCurrentStudyPlan();
1289
+
1290
+ showToast(`Welcome back, ${currentUser.email}!`, 'success', 3000);
1291
+ }
1292
+
1293
+ function showGuestState() {
1294
+ document.getElementById('guestAuth').style.display = 'flex';
1295
+ document.getElementById('userAuth').style.display = 'none';
1296
+
1297
+ // Update session status
1298
+ document.getElementById('guestStatus').style.display = 'inline';
1299
+ document.getElementById('userStatus').style.display = 'none';
1300
+ document.getElementById('loadFlashcardsBtn').style.display = 'none';
1301
+
1302
+ // Hide advanced panels
1303
+ document.getElementById('contentCuration').style.display = 'none';
1304
+ document.getElementById('contentCurationLogin').style.display = 'block';
1305
+ document.getElementById('analyticsPanel').style.display = 'none';
1306
+ document.getElementById('studyPlannerPanel').style.display = 'none';
1307
+
1308
+ currentUser = null;
1309
+ }
1310
+
1311
+ function loadUserSettings(settings) {
1312
+ if (settings.preferred_model) modelSelector.value = settings.preferred_model;
1313
+ if (settings.context_focus) contextSelector.value = settings.context_focus;
1314
+ if (settings.voice_accent) voiceSelector.value = settings.voice_accent;
1315
+ }
1316
+
1317
+ function showAuthModal(type = 'login') {
1318
+ document.getElementById('authModal').classList.add('show');
1319
+ switchAuthForm(type);
1320
+ }
1321
+
1322
+ function hideAuthModal() {
1323
+ document.getElementById('authModal').classList.remove('show');
1324
+ // Clear forms
1325
+ document.getElementById('loginForm').reset();
1326
+ document.getElementById('registerForm').reset();
1327
+ }
1328
+
1329
+ function switchAuthForm(type) {
1330
+ const loginForm = document.getElementById('loginForm');
1331
+ const registerForm = document.getElementById('registerForm');
1332
+ const title = document.getElementById('authModalTitle');
1333
+
1334
+ if (type === 'login') {
1335
+ loginForm.classList.add('active');
1336
+ registerForm.classList.remove('active');
1337
+ title.textContent = 'Login';
1338
+ } else {
1339
+ registerForm.classList.add('active');
1340
+ loginForm.classList.remove('active');
1341
+ title.textContent = 'Create Account';
1342
+ }
1343
+ }
1344
+
1345
+ function toggleUserMenu() {
1346
+ document.getElementById('userAuth').classList.toggle('show');
1347
+ }
1348
+
1349
+ // Close user menu when clicking outside
1350
+ document.addEventListener('click', (e) => {
1351
+ if (!e.target.closest('.user-menu')) {
1352
+ document.getElementById('userAuth').classList.remove('show');
1353
+ }
1354
+ });
1355
+
1356
+ // Login form submission
1357
+ document.getElementById('loginForm').addEventListener('submit', async (e) => {
1358
+ e.preventDefault();
1359
+ const email = document.getElementById('loginEmail').value;
1360
+ const password = document.getElementById('loginPassword').value;
1361
+
1362
+ const submitBtn = e.target.querySelector('button[type="submit"]');
1363
+ setButtonLoading(submitBtn, true);
1364
+
1365
+ try {
1366
+ const data = await handleApiRequest('/login', {
1367
+ method: 'POST',
1368
+ headers: { 'Content-Type': 'application/json' },
1369
+ body: JSON.stringify({ email, password })
1370
+ }, 'Logging in');
1371
+
1372
+ currentUser = data.user;
1373
+ hideAuthModal();
1374
+ showAuthenticatedState();
1375
+
1376
+ if (data.user.settings) {
1377
+ loadUserSettings(data.user.settings);
1378
+ }
1379
+
1380
+ } catch (error) {
1381
+ // Error already handled by handleApiRequest
1382
+ } finally {
1383
+ setButtonLoading(submitBtn, false);
1384
+ }
1385
+ });
1386
+
1387
+ // Register form submission
1388
+ document.getElementById('registerForm').addEventListener('submit', async (e) => {
1389
+ e.preventDefault();
1390
+ const email = document.getElementById('registerEmail').value;
1391
+ const password = document.getElementById('registerPassword').value;
1392
+ const confirmPassword = document.getElementById('confirmPassword').value;
1393
+
1394
+ if (password !== confirmPassword) {
1395
+ showToast('Passwords do not match', 'error');
1396
+ return;
1397
+ }
1398
+
1399
+ const submitBtn = e.target.querySelector('button[type="submit"]');
1400
+ setButtonLoading(submitBtn, true);
1401
+
1402
+ try {
1403
+ const data = await handleApiRequest('/register', {
1404
+ method: 'POST',
1405
+ headers: { 'Content-Type': 'application/json' },
1406
+ body: JSON.stringify({ email, password })
1407
+ }, 'Creating account');
1408
+
1409
+ hideAuthModal();
1410
+ showToast('Account created! Please check your email to confirm your account.', 'success', 8000);
1411
+
1412
+ } catch (error) {
1413
+ // Error already handled by handleApiRequest
1414
+ } finally {
1415
+ setButtonLoading(submitBtn, false);
1416
+ }
1417
+ });
1418
+
1419
+ async function logout() {
1420
+ try {
1421
+ await handleApiRequest('/logout', {
1422
+ method: 'POST'
1423
+ }, 'Logging out');
1424
+
1425
+ showGuestState();
1426
+ showToast('Logged out successfully', 'info');
1427
+
1428
+ } catch (error) {
1429
+ // Error already handled by handleApiRequest
1430
+ }
1431
+ }
1432
+
1433
+ async function showUserSettings() {
1434
+ if (!currentUser) return;
1435
+
1436
+ document.getElementById('userAuth').classList.remove('show');
1437
+
1438
+ // Simple settings update using current form values
1439
+ const settings = {
1440
+ preferred_model: modelSelector.value,
1441
+ context_focus: contextSelector.value,
1442
+ voice_accent: voiceSelector.value,
1443
+ daily_goal: 10, // Default
1444
+ notification_enabled: true // Default
1445
+ };
1446
+
1447
+ try {
1448
+ await handleApiRequest('/user/settings', {
1449
+ method: 'POST',
1450
+ headers: { 'Content-Type': 'application/json' },
1451
+ body: JSON.stringify(settings)
1452
+ }, 'Saving settings');
1453
+
1454
+ showToast('Settings saved successfully!', 'success');
1455
+ } catch (error) {
1456
+ // Error already handled
1457
+ }
1458
+ }
1459
+
1460
+ async function showUserFlashcards() {
1461
+ if (!currentUser) return;
1462
+
1463
+ document.getElementById('userAuth').classList.remove('show');
1464
+
1465
+ try {
1466
+ const data = await handleApiRequest('/user/flashcards', {}, 'Loading your flashcards');
1467
+
1468
+ // Clear current flashcards
1469
+ flashcardList.innerHTML = '';
1470
+
1471
+ if (data.flashcards && data.flashcards.length > 0) {
1472
+ data.flashcards.forEach(card => {
1473
+ const flashcardData = {
1474
+ term: card.term,
1475
+ translation: card.translation,
1476
+ context_sentence: card.context_sentence,
1477
+ gapped_sentence: card.gapped_sentence,
1478
+ definition: card.definition
1479
+ };
1480
+ renderFlashcard(flashcardData);
1481
+ });
1482
+ showToast(`Loaded ${data.flashcards.length} saved flashcards`, 'success');
1483
+ } else {
1484
+ noCardsMessage.classList.remove('hidden');
1485
+ showToast('No saved flashcards found', 'info');
1486
+ }
1487
+
1488
+ } catch (error) {
1489
+ noCardsMessage.classList.remove('hidden');
1490
+ }
1491
+ }
1492
+
1493
+ // --- CONTENT CURATION FUNCTIONS ---
1494
+
1495
+ async function searchContent() {
1496
+ if (!currentUser) {
1497
+ showToast('Please log in to search content', 'warning');
1498
+ return;
1499
+ }
1500
+
1501
+ const query = document.getElementById('contentSearchInput').value.trim();
1502
+ const category = document.getElementById('contentCategorySelect').value;
1503
+
1504
+ if (!query) {
1505
+ showToast('Please enter a search topic', 'warning');
1506
+ return;
1507
+ }
1508
+
1509
+ const resultsContainer = document.getElementById('contentResults');
1510
+ resultsContainer.innerHTML = '<div class="spinner-container"><div class="spinner spinner-large"></div><p class="ml-3">Searching for relevant content...</p></div>';
1511
+
1512
+ try {
1513
+ const data = await handleApiRequest('/content/search', {
1514
+ method: 'POST',
1515
+ headers: { 'Content-Type': 'application/json' },
1516
+ body: JSON.stringify({ query, category })
1517
+ }, 'Searching content');
1518
+
1519
+ displayContentResults(data.results);
1520
+ } catch (error) {
1521
+ resultsContainer.innerHTML = '<div class="text-red-600">Failed to search content. Please try again.</div>';
1522
+ }
1523
+ }
1524
+
1525
+ async function getRecommendations() {
1526
+ if (!currentUser) {
1527
+ showToast('Please log in to get recommendations', 'warning');
1528
+ return;
1529
+ }
1530
+
1531
+ const resultsContainer = document.getElementById('contentResults');
1532
+ resultsContainer.innerHTML = '<div class="spinner-container"><div class="spinner spinner-large"></div><p class="ml-3">Generating AI recommendations...</p></div>';
1533
+
1534
+ try {
1535
+ const data = await handleApiRequest('/content/recommendations', {}, 'Getting recommendations');
1536
+ displayRecommendations(data.recommendations);
1537
+ } catch (error) {
1538
+ resultsContainer.innerHTML = '<div class="text-red-600">Failed to get recommendations. Please try again.</div>';
1539
+ }
1540
+ }
1541
+
1542
+ function displayContentResults(results) {
1543
+ const container = document.getElementById('contentResults');
1544
+
1545
+ if (!results || results.length === 0) {
1546
+ container.innerHTML = `
1547
+ <div class="empty-state">
1548
+ <div class="empty-state-icon">πŸ”</div>
1549
+ <div class="empty-state-title">No Content Found</div>
1550
+ <div class="empty-state-description">Try different search terms or check your interests settings</div>
1551
+ </div>`;
1552
+ return;
1553
+ }
1554
+
1555
+ container.innerHTML = results.map(result => `
1556
+ <div class="content-item fade-in">
1557
+ <div class="content-item-title">${result.title}</div>
1558
+ <div class="content-item-meta">
1559
+ <span class="difficulty-badge difficulty-${getDifficultyClass(result.estimated_difficulty || 3)}">
1560
+ Level ${result.estimated_difficulty || 3}
1561
+ </span>
1562
+ <span class="ml-2">${result.source}</span>
1563
+ ${result.published ? `<span class="ml-2">${new Date(result.published).toLocaleDateString()}</span>` : ''}
1564
+ </div>
1565
+ <div class="content-item-summary">${result.summary}</div>
1566
+ <div class="flex gap-2 mt-3">
1567
+ <button onclick="extractAndSaveContent('${result.url}', '${result.title.replace(/'/g, "\\'")}')" class="btn bg-green-500 hover:bg-green-600 text-white text-sm">πŸ“– Read & Save</button>
1568
+ <a href="${result.url}" target="_blank" class="btn bg-gray-500 hover:bg-gray-600 text-white text-sm">πŸ”— Open Original</a>
1569
+ </div>
1570
+ </div>
1571
+ `).join('');
1572
+ }
1573
+
1574
+ function displayRecommendations(recommendations) {
1575
+ const container = document.getElementById('contentResults');
1576
+
1577
+ if (!recommendations || recommendations.length === 0) {
1578
+ container.innerHTML = `
1579
+ <div class="empty-state">
1580
+ <div class="empty-state-icon">πŸ€–</div>
1581
+ <div class="empty-state-title">No Recommendations</div>
1582
+ <div class="empty-state-description">Update your interests in settings to get better recommendations</div>
1583
+ </div>`;
1584
+ return;
1585
+ }
1586
+
1587
+ container.innerHTML = `
1588
+ <h3 class="text-lg font-semibold mb-4 text-purple-600">✨ AI Recommendations for You</h3>
1589
+ ` + recommendations.map(rec => `
1590
+ <div class="content-item fade-in border-purple-200">
1591
+ <div class="content-item-title text-purple-700">${rec.topic}</div>
1592
+ <div class="content-item-meta">
1593
+ <span class="difficulty-badge difficulty-${getDifficultyClass(rec.difficulty)}">
1594
+ ${rec.difficulty}
1595
+ </span>
1596
+ <span class="ml-2">AI Recommended</span>
1597
+ </div>
1598
+ <div class="content-item-summary">${rec.reason}</div>
1599
+ <div class="flex gap-2 mt-3">
1600
+ <button onclick="searchSpecificTopic('${rec.topic.replace(/'/g, "\\'")}')" class="btn bg-purple-500 hover:bg-purple-600 text-white text-sm">πŸ” Find Articles</button>
1601
+ </div>
1602
+ </div>
1603
+ `).join('');
1604
+ }
1605
+
1606
+ function getDifficultyClass(level) {
1607
+ if (level <= 2) return 'easy';
1608
+ if (level <= 4) return 'medium';
1609
+ return 'hard';
1610
+ }
1611
+
1612
+ async function extractAndSaveContent(url, title) {
1613
+ try {
1614
+ showToast('Extracting content...', 'info', 2000);
1615
+
1616
+ const extractData = await handleApiRequest('/content/extract', {
1617
+ method: 'POST',
1618
+ headers: { 'Content-Type': 'application/json' },
1619
+ body: JSON.stringify({ url })
1620
+ }, 'Extracting content');
1621
+
1622
+ if (extractData.success) {
1623
+ const saveData = await handleApiRequest('/content/save', {
1624
+ method: 'POST',
1625
+ headers: { 'Content-Type': 'application/json' },
1626
+ body: JSON.stringify({
1627
+ title: extractData.title || title,
1628
+ content: extractData.content,
1629
+ source_url: url,
1630
+ source_type: 'web_search'
1631
+ })
1632
+ }, 'Saving content');
1633
+
1634
+ showToast('Content saved successfully!', 'success');
1635
+ loadAnalytics(); // Refresh analytics
1636
+ }
1637
+ } catch (error) {
1638
+ // Error already handled by handleApiRequest
1639
+ }
1640
+ }
1641
+
1642
+ async function saveCustomContent() {
1643
+ if (!currentUser) {
1644
+ showToast('Please log in to save content', 'warning');
1645
+ return;
1646
+ }
1647
+
1648
+ const title = document.getElementById('customContentTitle').value.trim();
1649
+ const content = document.getElementById('customContentText').value.trim();
1650
+ const url = document.getElementById('customContentUrl').value.trim();
1651
+
1652
+ if (!title || !content) {
1653
+ showToast('Please provide both title and content', 'warning');
1654
+ return;
1655
+ }
1656
+
1657
+ try {
1658
+ await handleApiRequest('/content/save', {
1659
+ method: 'POST',
1660
+ headers: { 'Content-Type': 'application/json' },
1661
+ body: JSON.stringify({
1662
+ title,
1663
+ content,
1664
+ source_url: url || null,
1665
+ source_type: 'manual'
1666
+ })
1667
+ }, 'Saving content');
1668
+
1669
+ // Clear form
1670
+ document.getElementById('customContentTitle').value = '';
1671
+ document.getElementById('customContentText').value = '';
1672
+ document.getElementById('customContentUrl').value = '';
1673
+
1674
+ showToast('Content saved successfully!', 'success');
1675
+ loadAnalytics(); // Refresh analytics
1676
+ } catch (error) {
1677
+ // Error already handled
1678
+ }
1679
+ }
1680
+
1681
+ async function showSavedArticles() {
1682
+ if (!currentUser) return;
1683
+
1684
+ try {
1685
+ const data = await handleApiRequest('/content/articles', {}, 'Loading saved articles');
1686
+
1687
+ const container = document.getElementById('contentResults');
1688
+
1689
+ if (!data.articles || data.articles.length === 0) {
1690
+ container.innerHTML = `
1691
+ <div class="empty-state">
1692
+ <div class="empty-state-icon">πŸ“š</div>
1693
+ <div class="empty-state-title">No Saved Articles</div>
1694
+ <div class="empty-state-description">Search and save articles to build your personal library</div>
1695
+ </div>`;
1696
+ return;
1697
+ }
1698
+
1699
+ container.innerHTML = `
1700
+ <h3 class="text-lg font-semibold mb-4 text-green-600">πŸ“š Your Saved Articles (${data.articles.length})</h3>
1701
+ ` + data.articles.map(article => `
1702
+ <div class="content-item fade-in">
1703
+ <div class="content-item-title">${article.title}</div>
1704
+ <div class="content-item-meta">
1705
+ <span class="difficulty-badge difficulty-medium">${article.word_count} words</span>
1706
+ <span class="ml-2">${new Date(article.created_at).toLocaleDateString()}</span>
1707
+ ${article.category ? `<span class="ml-2">${article.category}</span>` : ''}
1708
+ </div>
1709
+ <div class="content-item-summary">${article.content.substring(0, 150)}...</div>
1710
+ <div class="flex gap-2 mt-3">
1711
+ <button onclick="loadArticleForStudy('${article.id}', '${article.title.replace(/'/g, "\\'")}')" class="btn btn-primary text-sm">πŸ“– Study This</button>
1712
+ ${article.source_url ? `<a href="${article.source_url}" target="_blank" class="btn bg-gray-500 hover:bg-gray-600 text-white text-sm">πŸ”— Original</a>` : ''}
1713
+ </div>
1714
+ </div>
1715
+ `).join('');
1716
+ } catch (error) {
1717
+ // Error already handled
1718
+ }
1719
+ }
1720
+
1721
+ function loadArticleForStudy(articleId, title) {
1722
+ // Load the article content into the text editor
1723
+ showToast(`Loading "${title}" for study...`, 'info');
1724
+ // This would load the full article content - simplified for now
1725
+ showToast('Article loaded! You can now create flashcards from it.', 'success');
1726
+ }
1727
+
1728
+ function searchSpecificTopic(topic) {
1729
+ document.getElementById('contentSearchInput').value = topic;
1730
+ searchContent();
1731
+ }
1732
+
1733
+ // --- ANALYTICS FUNCTIONS ---
1734
+
1735
+ async function loadAnalytics() {
1736
+ if (!currentUser) return;
1737
+
1738
+ try {
1739
+ const data = await handleApiRequest('/analytics/dashboard', {}, 'Loading analytics');
1740
+
1741
+ document.getElementById('totalFlashcards').textContent = data.analytics.total_flashcards || 0;
1742
+ document.getElementById('totalArticles').textContent = data.analytics.total_articles || 0;
1743
+ document.getElementById('currentLevel').textContent = data.analytics.user_level || 'B1';
1744
+ } catch (error) {
1745
+ // Error already handled
1746
+ }
1747
+ }
1748
+
1749
+ function toggleAnalytics() {
1750
+ const content = document.getElementById('analyticsContent');
1751
+ content.style.display = content.style.display === 'none' ? 'block' : 'none';
1752
+ }
1753
+
1754
+ // --- STUDY PLANNER FUNCTIONS ---
1755
+
1756
+ async function generateStudyPlan() {
1757
+ if (!currentUser) return;
1758
+
1759
+ try {
1760
+ const planData = {
1761
+ current_level: currentUser.english_level || 'B1',
1762
+ target_level: document.getElementById('targetLevelSelect')?.value || 'B2',
1763
+ weekly_hours: parseInt(document.getElementById('weeklyHoursSelect')?.value || 5),
1764
+ context_focus: currentUser.context_focus || 'General/Social',
1765
+ study_goals: []
1766
+ };
1767
+
1768
+ showLoading('Generating your personalized study plan...');
1769
+
1770
+ const response = await fetch('/study-plan/create', {
1771
+ method: 'POST',
1772
+ headers: {
1773
+ 'Content-Type': 'application/json',
1774
+ },
1775
+ body: JSON.stringify(planData)
1776
+ });
1777
+
1778
+ const result = await response.json();
1779
+ hideLoading();
1780
+
1781
+ if (result.success) {
1782
+ displayStudyPlan(result.plan);
1783
+ showToast('Study plan generated successfully!', 'success');
1784
+ } else {
1785
+ showToast('Failed to generate study plan: ' + result.error, 'error');
1786
+ }
1787
+ } catch (error) {
1788
+ hideLoading();
1789
+ console.error('Error generating study plan:', error);
1790
+ showToast('Error generating study plan', 'error');
1791
+ }
1792
+ }
1793
+
1794
+ async function loadCurrentStudyPlan() {
1795
+ if (!currentUser) return;
1796
+
1797
+ try {
1798
+ const response = await fetch('/study-plan/current');
1799
+ const result = await response.json();
1800
+
1801
+ if (result.success && result.plan) {
1802
+ displayStudyPlan(result.plan);
1803
+ loadStudyProgress();
1804
+ }
1805
+ } catch (error) {
1806
+ console.error('Error loading study plan:', error);
1807
+ }
1808
+ }
1809
+
1810
+ async function loadStudyProgress() {
1811
+ if (!currentUser) return;
1812
+
1813
+ try {
1814
+ const response = await fetch('/study-plan/progress');
1815
+ const result = await response.json();
1816
+
1817
+ if (result.success && result.progress) {
1818
+ displayStudyProgress(result.progress);
1819
+ }
1820
+ } catch (error) {
1821
+ console.error('Error loading study progress:', error);
1822
+ }
1823
+ }
1824
+
1825
+ async function completeActivity(planId, activityId, durationMinutes) {
1826
+ if (!currentUser) return;
1827
+
1828
+ try {
1829
+ const notes = prompt('Any notes about this activity? (optional)') || '';
1830
+
1831
+ const response = await fetch('/study-plan/activity/complete', {
1832
+ method: 'POST',
1833
+ headers: {
1834
+ 'Content-Type': 'application/json',
1835
+ },
1836
+ body: JSON.stringify({
1837
+ plan_id: planId,
1838
+ activity_id: activityId,
1839
+ duration_minutes: durationMinutes,
1840
+ notes: notes
1841
+ })
1842
+ });
1843
+
1844
+ const result = await response.json();
1845
+
1846
+ if (result.success) {
1847
+ showToast('Activity completed successfully!', 'success');
1848
+
1849
+ // Update the UI
1850
+ const activityCard = document.querySelector(`[data-activity-id="${activityId}"]`);
1851
+ if (activityCard) {
1852
+ activityCard.classList.add('completed');
1853
+ const button = activityCard.querySelector('.complete-activity-btn');
1854
+ if (button) {
1855
+ button.textContent = 'βœ… Completed';
1856
+ button.disabled = true;
1857
+ }
1858
+ }
1859
+
1860
+ // Refresh progress
1861
+ loadStudyProgress();
1862
+ } else {
1863
+ showToast('Failed to complete activity', 'error');
1864
+ }
1865
+ } catch (error) {
1866
+ console.error('Error completing activity:', error);
1867
+ showToast('Error completing activity', 'error');
1868
+ }
1869
+ }
1870
+
1871
+ function displayStudyPlan(plan) {
1872
+ const container = document.getElementById('studyPlanResult');
1873
+ if (!container) return;
1874
+
1875
+ container.innerHTML = `
1876
+ <div class="study-plan-container">
1877
+ <div class="study-plan-header">
1878
+ <h3>πŸ“… Your Personalized Study Plan</h3>
1879
+ <div class="plan-overview grid grid-cols-2 gap-4 mt-4">
1880
+ <div class="plan-stat">
1881
+ <strong>Current Level:</strong> ${plan.current_level}
1882
+ </div>
1883
+ <div class="plan-stat">
1884
+ <strong>Target Level:</strong> ${plan.target_level}
1885
+ </div>
1886
+ <div class="plan-stat">
1887
+ <strong>Weekly Hours:</strong> ${plan.weekly_hours}h
1888
+ </div>
1889
+ <div class="plan-stat">
1890
+ <strong>Duration:</strong> ${plan.estimated_weeks} weeks
1891
+ </div>
1892
+ </div>
1893
+ </div>
1894
+
1895
+ <div class="study-tips mt-6">
1896
+ <h4 class="font-medium mb-2">πŸ’‘ Personalized Study Tips</h4>
1897
+ <ul class="space-y-1">
1898
+ ${plan.study_tips.map(tip => `<li class="text-sm text-gray-600">${tip}</li>`).join('')}
1899
+ </ul>
1900
+ </div>
1901
+
1902
+ <div class="weekly-activities mt-6">
1903
+ <h4 class="font-medium mb-3">πŸ“š Weekly Activities</h4>
1904
+ <div class="activities-grid space-y-3">
1905
+ ${plan.activities.slice(0, 6).map(activity => `
1906
+ <div class="activity-card bg-gray-50 p-3 rounded-lg" data-activity-id="${activity.id}">
1907
+ <div class="activity-header flex items-center gap-2 mb-2">
1908
+ <span class="text-lg">${activity.icon}</span>
1909
+ <strong class="text-sm">${activity.title}</strong>
1910
+ </div>
1911
+ <div class="activity-details text-xs text-gray-600">
1912
+ <p><strong>Duration:</strong> ${activity.duration_minutes} minutes</p>
1913
+ <p><strong>Day:</strong> ${['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][activity.day_of_week]}</p>
1914
+ </div>
1915
+ <button class="complete-activity-btn mt-2 bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600"
1916
+ onclick="completeActivity('${plan.db_id || plan.id}', '${activity.id}', ${activity.duration_minutes})">
1917
+ βœ… Mark Complete
1918
+ </button>
1919
+ </div>
1920
+ `).join('')}
1921
+ </div>
1922
+ </div>
1923
+
1924
+ <div class="milestones mt-6">
1925
+ <h4 class="font-medium mb-3">🎯 Study Milestones</h4>
1926
+ <div class="milestones-list space-y-2">
1927
+ ${plan.milestones.slice(0, 3).map(milestone => `
1928
+ <div class="milestone-item flex items-center gap-3 p-2 bg-blue-50 rounded ${milestone.completed ? 'completed opacity-75' : ''}">
1929
+ <div class="milestone-week bg-blue-500 text-white px-2 py-1 rounded text-xs">Week ${milestone.week}</div>
1930
+ <div class="milestone-content flex-1">
1931
+ <strong class="text-sm">${milestone.title}</strong>
1932
+ <p class="text-xs text-gray-600">${milestone.description}</p>
1933
+ </div>
1934
+ </div>
1935
+ `).join('')}
1936
+ </div>
1937
+ </div>
1938
+ </div>
1939
+ `;
1940
+
1941
+ container.classList.remove('hidden');
1942
+ }
1943
+
1944
+ function displayStudyProgress(progress) {
1945
+ // This would be called to show progress in analytics section
1946
+ console.log('Study progress:', progress);
1947
+ }
1948
+
1949
+ // --- INICIALIZAÇÃO ---
1950
+ document.addEventListener('DOMContentLoaded', () => {
1951
+ checkAuthStatus();
1952
+ populateModelSelector();
1953
+ initializeSpeechRecognition();
1954
+ });
1955
+ </script>
1956
+
1957
+ </body>
1958
+ </html>
1959
+
requeriments.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ flask-session
3
+ gtts
4
+ google-generativeai
5
+ groq
6
+ requests
7
+ Pillow
8
+ werkzeug
9
+ email-validator
10
+ beautifulsoup4
11
+ feedparser
12
+ matplotlib
13
+ plotly
14
+ pandas
15
+ numpy
16
+ psutil
run.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # run.py - Production startup script for English Helper
2
+
3
+ import os
4
+ import sys
5
+ from app import app
6
+ from database import init_db
7
+
8
+ def setup_environment():
9
+ """Setup environment variables and configuration"""
10
+ # Set default environment variables if not present
11
+ if not os.environ.get('SECRET_KEY'):
12
+ os.environ['SECRET_KEY'] = 'dev-key-change-in-production'
13
+
14
+ # Create data directory if it doesn't exist
15
+ data_dir = os.path.join(os.path.dirname(__file__), 'data')
16
+ if not os.path.exists(data_dir):
17
+ os.makedirs(data_dir)
18
+
19
+ print("βœ… Environment setup complete")
20
+
21
+ def initialize_database():
22
+ """Initialize the database"""
23
+ try:
24
+ init_db()
25
+ print("βœ… Database initialized successfully")
26
+ except Exception as e:
27
+ print(f"❌ Database initialization failed: {e}")
28
+ sys.exit(1)
29
+
30
+ def check_dependencies():
31
+ """Check if all required dependencies are installed"""
32
+ required_packages = [
33
+ 'flask', 'flask_session', 'gtts', 'groq', 'google.generativeai',
34
+ 'requests', 'PIL', 'werkzeug', 'email_validator', 'beautifulsoup4',
35
+ 'feedparser', 'matplotlib', 'plotly', 'pandas', 'numpy'
36
+ ]
37
+
38
+ missing_packages = []
39
+ for package in required_packages:
40
+ try:
41
+ if package == 'PIL':
42
+ import PIL
43
+ elif package == 'google.generativeai':
44
+ import google.generativeai
45
+ else:
46
+ __import__(package)
47
+ except ImportError:
48
+ missing_packages.append(package)
49
+
50
+ if missing_packages:
51
+ print(f"❌ Missing required packages: {', '.join(missing_packages)}")
52
+ print("Install them with: pip install -r requirements.txt")
53
+ return False
54
+
55
+ print("βœ… All dependencies are installed")
56
+ return True
57
+
58
+ def startup_checks():
59
+ """Perform startup checks"""
60
+ print("πŸš€ Starting English Helper...")
61
+ print("=" * 50)
62
+
63
+ # Check dependencies
64
+ if not check_dependencies():
65
+ print("Please install missing dependencies and try again.")
66
+ sys.exit(1)
67
+
68
+ # Setup environment
69
+ setup_environment()
70
+
71
+ # Initialize database
72
+ initialize_database()
73
+
74
+ print("=" * 50)
75
+ print("βœ… All startup checks passed!")
76
+ print("🌐 Starting Flask application...")
77
+
78
+ if __name__ == '__main__':
79
+ startup_checks()
80
+
81
+ # Get configuration from environment
82
+ debug = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
83
+ port = int(os.environ.get('PORT', 5000))
84
+ host = os.environ.get('HOST', '127.0.0.1')
85
+
86
+ print(f"πŸ“‘ Server starting on http://{host}:{port}")
87
+ print("🎯 Press Ctrl+C to stop the server")
88
+
89
+ # Start the Flask application
90
+ app.run(
91
+ host=host,
92
+ port=port,
93
+ debug=debug,
94
+ threaded=True
95
+ )
study_planner.py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # study_planner.py
2
+ import json
3
+ from datetime import datetime, timedelta, date
4
+ from typing import Dict, List, Any
5
+ import logging
6
+ from groq import Groq
7
+ import google.generativeai as genai
8
+ import os
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Initialize AI clients (reuse from main app)
13
+ groq_client = None
14
+ genai_client = None
15
+
16
+ try:
17
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
18
+ if GROQ_API_KEY:
19
+ groq_client = Groq(api_key=GROQ_API_KEY)
20
+ except Exception as e:
21
+ logger.warning(f"Groq client not available: {e}")
22
+
23
+ try:
24
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
25
+ if GEMINI_API_KEY:
26
+ genai.configure(api_key=GEMINI_API_KEY)
27
+ genai_client = genai
28
+ except Exception as e:
29
+ logger.warning(f"Gemini client not available: {e}")
30
+
31
+ class StudyPlanner:
32
+ def __init__(self):
33
+ self.level_progression = {
34
+ 'A1': {'next': 'A2', 'weeks': 12, 'focus': ['basic vocabulary', 'present tense', 'introductions']},
35
+ 'A2': {'next': 'B1', 'weeks': 16, 'focus': ['past tense', 'future tense', 'everyday situations']},
36
+ 'B1': {'next': 'B2', 'weeks': 20, 'focus': ['conditional', 'complex sentences', 'opinions']},
37
+ 'B2': {'next': 'C1', 'weeks': 24, 'focus': ['subjunctive', 'formal writing', 'presentations']},
38
+ 'C1': {'next': 'C2', 'weeks': 28, 'focus': ['nuanced expressions', 'academic writing', 'debates']},
39
+ 'C2': {'next': 'C2', 'weeks': 32, 'focus': ['native-like fluency', 'specialized topics', 'literature']}
40
+ }
41
+
42
+ self.activity_types = {
43
+ 'reading': {
44
+ 'icon': 'πŸ“š',
45
+ 'min_duration': 20,
46
+ 'max_duration': 45,
47
+ 'difficulty_scaling': True,
48
+ 'description': 'Read articles and texts'
49
+ },
50
+ 'flashcards': {
51
+ 'icon': 'πŸƒ',
52
+ 'min_duration': 10,
53
+ 'max_duration': 25,
54
+ 'difficulty_scaling': False,
55
+ 'description': 'Review vocabulary flashcards'
56
+ },
57
+ 'conversation': {
58
+ 'icon': 'πŸ’¬',
59
+ 'min_duration': 15,
60
+ 'max_duration': 30,
61
+ 'difficulty_scaling': True,
62
+ 'description': 'Practice speaking and conversation'
63
+ },
64
+ 'writing': {
65
+ 'icon': '✍️',
66
+ 'min_duration': 15,
67
+ 'max_duration': 40,
68
+ 'difficulty_scaling': True,
69
+ 'description': 'Complete writing exercises'
70
+ },
71
+ 'listening': {
72
+ 'icon': '🎧',
73
+ 'min_duration': 15,
74
+ 'max_duration': 30,
75
+ 'difficulty_scaling': True,
76
+ 'description': 'Listen to audio content'
77
+ },
78
+ 'grammar': {
79
+ 'icon': 'πŸ“',
80
+ 'min_duration': 10,
81
+ 'max_duration': 25,
82
+ 'difficulty_scaling': True,
83
+ 'description': 'Study grammar rules and patterns'
84
+ }
85
+ }
86
+
87
+ def generate_personalized_plan(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
88
+ """Generate a comprehensive study plan based on user data"""
89
+ try:
90
+ current_level = user_data.get('english_level', 'B1')
91
+ target_level = user_data.get('target_level', 'B2')
92
+ weekly_hours = user_data.get('weekly_hours', 5)
93
+ interests = user_data.get('interests', {})
94
+ context_focus = user_data.get('context_focus', 'General/Social')
95
+ study_goals = user_data.get('study_goals', [])
96
+
97
+ # Calculate timeline
98
+ timeline = self._calculate_study_timeline(current_level, target_level, weekly_hours)
99
+
100
+ # Generate weekly structure
101
+ weekly_structure = self._create_weekly_structure(weekly_hours, current_level, context_focus)
102
+
103
+ # Create specific activities
104
+ activities = self._generate_weekly_activities(
105
+ weekly_structure, interests, current_level, context_focus, study_goals
106
+ )
107
+
108
+ # Generate AI-powered study tips
109
+ study_tips = self._generate_ai_study_tips(user_data)
110
+
111
+ plan = {
112
+ 'id': f"plan_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
113
+ 'created_at': datetime.now().isoformat(),
114
+ 'current_level': current_level,
115
+ 'target_level': target_level,
116
+ 'weekly_hours': weekly_hours,
117
+ 'estimated_weeks': timeline['weeks'],
118
+ 'completion_date': timeline['completion_date'],
119
+ 'weekly_structure': weekly_structure,
120
+ 'activities': activities,
121
+ 'study_tips': study_tips,
122
+ 'milestones': self._create_milestones(current_level, target_level, timeline['weeks']),
123
+ 'adaptations': self._suggest_adaptations(user_data)
124
+ }
125
+
126
+ return {'success': True, 'plan': plan}
127
+
128
+ except Exception as e:
129
+ logger.error(f"Error generating study plan: {e}")
130
+ return {'success': False, 'error': str(e)}
131
+
132
+ def _calculate_study_timeline(self, current_level: str, target_level: str, weekly_hours: int) -> Dict[str, Any]:
133
+ """Calculate realistic timeline for reaching target level"""
134
+ try:
135
+ current_info = self.level_progression.get(current_level, self.level_progression['B1'])
136
+ base_weeks = current_info['weeks']
137
+
138
+ # Adjust based on weekly hours (baseline is 5 hours/week)
139
+ hour_multiplier = 5 / max(weekly_hours, 1)
140
+ adjusted_weeks = int(base_weeks * hour_multiplier)
141
+
142
+ # If targeting multiple levels ahead, add additional time
143
+ level_order = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
144
+ current_idx = level_order.index(current_level) if current_level in level_order else 2
145
+ target_idx = level_order.index(target_level) if target_level in level_order else 3
146
+
147
+ if target_idx > current_idx + 1:
148
+ # Multiple levels - add 20% more time
149
+ adjusted_weeks = int(adjusted_weeks * 1.2 * (target_idx - current_idx))
150
+
151
+ completion_date = (datetime.now() + timedelta(weeks=adjusted_weeks)).date()
152
+
153
+ return {
154
+ 'weeks': adjusted_weeks,
155
+ 'completion_date': completion_date.isoformat(),
156
+ 'intensity': 'High' if weekly_hours > 7 else 'Medium' if weekly_hours > 4 else 'Light'
157
+ }
158
+
159
+ except Exception as e:
160
+ logger.error(f"Error calculating timeline: {e}")
161
+ return {'weeks': 16, 'completion_date': (datetime.now() + timedelta(weeks=16)).date().isoformat()}
162
+
163
+ def _create_weekly_structure(self, weekly_hours: int, level: str, context: str) -> Dict[str, Any]:
164
+ """Create optimal weekly study structure"""
165
+ try:
166
+ # Base distribution percentages
167
+ distributions = {
168
+ 'A1': {'reading': 0.25, 'flashcards': 0.30, 'conversation': 0.20, 'writing': 0.15, 'grammar': 0.10},
169
+ 'A2': {'reading': 0.30, 'flashcards': 0.25, 'conversation': 0.20, 'writing': 0.15, 'grammar': 0.10},
170
+ 'B1': {'reading': 0.30, 'flashcards': 0.20, 'conversation': 0.25, 'writing': 0.20, 'listening': 0.05},
171
+ 'B2': {'reading': 0.25, 'flashcards': 0.15, 'conversation': 0.25, 'writing': 0.25, 'listening': 0.10},
172
+ 'C1': {'reading': 0.30, 'flashcards': 0.10, 'conversation': 0.25, 'writing': 0.25, 'listening': 0.10},
173
+ 'C2': {'reading': 0.35, 'flashcards': 0.05, 'conversation': 0.25, 'writing': 0.25, 'listening': 0.10}
174
+ }
175
+
176
+ base_dist = distributions.get(level, distributions['B1'])
177
+
178
+ # Adjust based on context
179
+ if context == 'Professional/Business':
180
+ base_dist['writing'] = min(base_dist['writing'] + 0.10, 0.40)
181
+ base_dist['reading'] = max(base_dist['reading'] - 0.05, 0.15)
182
+ base_dist['conversation'] = max(base_dist['conversation'] - 0.05, 0.15)
183
+ elif context == 'Technical/IT':
184
+ base_dist['reading'] = min(base_dist['reading'] + 0.10, 0.45)
185
+ base_dist['flashcards'] = min(base_dist['flashcards'] + 0.05, 0.35)
186
+ base_dist['conversation'] = max(base_dist['conversation'] - 0.10, 0.15)
187
+
188
+ # Convert to actual hours
189
+ weekly_structure = {}
190
+ total_minutes = weekly_hours * 60
191
+
192
+ for activity, percentage in base_dist.items():
193
+ minutes = int(total_minutes * percentage)
194
+ if minutes >= self.activity_types[activity]['min_duration']:
195
+ weekly_structure[activity] = {
196
+ 'minutes_per_week': minutes,
197
+ 'sessions_per_week': max(1, minutes // 30), # Aim for 30-min sessions
198
+ 'minutes_per_session': minutes // max(1, minutes // 30)
199
+ }
200
+
201
+ return weekly_structure
202
+
203
+ except Exception as e:
204
+ logger.error(f"Error creating weekly structure: {e}")
205
+ return {}
206
+
207
+ def _generate_weekly_activities(self, structure: Dict, interests: Dict, level: str, context: str, goals: List) -> List[Dict]:
208
+ """Generate specific weekly activities"""
209
+ activities = []
210
+
211
+ try:
212
+ for activity_type, schedule in structure.items():
213
+ activity_info = self.activity_types[activity_type]
214
+
215
+ for session in range(schedule['sessions_per_week']):
216
+ activity = {
217
+ 'id': f"{activity_type}_{session + 1}",
218
+ 'type': activity_type,
219
+ 'icon': activity_info['icon'],
220
+ 'title': f"{activity_info['description']}",
221
+ 'duration_minutes': schedule['minutes_per_session'],
222
+ 'difficulty': level,
223
+ 'context': context,
224
+ 'day_of_week': (session * 2) % 7, # Spread throughout week
225
+ 'specific_tasks': self._generate_specific_tasks(activity_type, level, context, interests, goals)
226
+ }
227
+ activities.append(activity)
228
+
229
+ # Sort by day of week
230
+ activities.sort(key=lambda x: x['day_of_week'])
231
+
232
+ return activities
233
+
234
+ except Exception as e:
235
+ logger.error(f"Error generating activities: {e}")
236
+ return []
237
+
238
+ def _generate_specific_tasks(self, activity_type: str, level: str, context: str, interests: Dict, goals: List) -> List[str]:
239
+ """Generate specific tasks for each activity type"""
240
+ tasks = []
241
+
242
+ try:
243
+ interest_topics = list(interests.keys())[:3] if interests else ['general topics']
244
+
245
+ if activity_type == 'reading':
246
+ tasks = [
247
+ f"Read a {context.lower()} article about {topic}" for topic in interest_topics
248
+ ] + [
249
+ f"Practice reading comprehension with {level}-level texts",
250
+ "Identify new vocabulary and create flashcards"
251
+ ]
252
+
253
+ elif activity_type == 'flashcards':
254
+ tasks = [
255
+ "Review previous day's vocabulary",
256
+ "Practice new words from recent reading",
257
+ f"Focus on {context.lower()} terminology"
258
+ ]
259
+
260
+ elif activity_type == 'conversation':
261
+ tasks = [
262
+ f"Discuss {topic} using {level}-level vocabulary" for topic in interest_topics[:2]
263
+ ] + [
264
+ "Practice pronunciation with AI feedback",
265
+ f"Role-play {context.lower()} scenarios"
266
+ ]
267
+
268
+ elif activity_type == 'writing':
269
+ tasks = [
270
+ f"Write a short text about {topic}" for topic in interest_topics[:1]
271
+ ] + [
272
+ f"Practice {context.lower()} writing format s",
273
+ "Get AI feedback on grammar and style"
274
+ ]
275
+
276
+ elif activity_type == 'listening':
277
+ tasks = [
278
+ f"Listen to content about {topic}" for topic in interest_topics[:2]
279
+ ] + [
280
+ "Practice with different accents",
281
+ "Take notes while listening"
282
+ ]
283
+
284
+ elif activity_type == 'grammar':
285
+ level_grammar = {
286
+ 'A1': ['present tense', 'basic sentence structure', 'personal pronouns'],
287
+ 'A2': ['past tense', 'future tense', 'comparatives'],
288
+ 'B1': ['present perfect', 'conditional sentences', 'passive voice'],
289
+ 'B2': ['subjunctive mood', 'complex sentences', 'reported speech'],
290
+ 'C1': ['advanced tenses', 'nuanced expressions', 'formal structures'],
291
+ 'C2': ['idiomatic expressions', 'stylistic variations', 'literary devices']
292
+ }
293
+
294
+ tasks = [f"Study {topic}" for topic in level_grammar.get(level, level_grammar['B1'])]
295
+
296
+ return tasks[:3] # Limit to 3 tasks per activity
297
+
298
+ except Exception as e:
299
+ logger.error(f"Error generating specific tasks: {e}")
300
+ return ["Complete activity as planned"]
301
+
302
+ def _generate_ai_study_tips(self, user_data: Dict) -> List[str]:
303
+ """Generate personalized study tips using AI"""
304
+ try:
305
+ if not groq_client and not genai_client:
306
+ return self._get_default_tips(user_data.get('english_level', 'B1'))
307
+
308
+ prompt = f"""
309
+ Generate 5 personalized English study tips for a user with these characteristics:
310
+ - Current Level: {user_data.get('english_level', 'B1')}
311
+ - Target Level: {user_data.get('target_level', 'B2')}
312
+ - Weekly Study Time: {user_data.get('weekly_hours', 5)} hours
313
+ - Context Focus: {user_data.get('context_focus', 'General/Social')}
314
+ - Interests: {', '.join(user_data.get('interests', {}).keys())}
315
+
316
+ Provide practical, actionable tips that are specific to their level and interests.
317
+ Format as a simple list of tips, each starting with an emoji.
318
+ """
319
+
320
+ response_text = None
321
+ if groq_client:
322
+ response = groq_client.chat.completions.create(
323
+ model="llama-3.1-8b-instant",
324
+ messages=[{"role": "user", "content": prompt}],
325
+ temperature=0.7
326
+ )
327
+ response_text = response.choices[0].message.content
328
+ elif genai_client:
329
+ model = genai_client.GenerativeModel('gemini-2.5-flash-latest')
330
+ response = model.generate_content(prompt)
331
+ response_text = response.text
332
+
333
+ if response_text:
334
+ # Extract tips from response
335
+ tips = [line.strip() for line in response_text.split('\n') if line.strip() and ('πŸ“š' in line or 'πŸ’‘' in line or '🎯' in line or '⭐' in line or 'πŸš€' in line)]
336
+ return tips[:5] if tips else self._get_default_tips(user_data.get('english_level', 'B1'))
337
+
338
+ return self._get_default_tips(user_data.get('english_level', 'B1'))
339
+
340
+ except Exception as e:
341
+ logger.error(f"Error generating AI study tips: {e}")
342
+ return self._get_default_tips(user_data.get('english_level', 'B1'))
343
+
344
+ def _get_default_tips(self, level: str) -> List[str]:
345
+ """Get default study tips based on level"""
346
+ tips_by_level = {
347
+ 'A1': [
348
+ "πŸ“š Start with basic vocabulary - 10 new words daily",
349
+ "🎯 Focus on present tense in daily conversations",
350
+ "πŸ’‘ Use picture dictionaries for visual learning",
351
+ "⭐ Practice pronunciation with simple audio materials",
352
+ "πŸš€ Don't worry about mistakes - communication is key!"
353
+ ],
354
+ 'A2': [
355
+ "πŸ“š Read simple news articles and stories",
356
+ "🎯 Practice past and future tenses regularly",
357
+ "πŸ’‘ Join basic English conversation groups",
358
+ "⭐ Use language learning apps for daily practice",
359
+ "πŸš€ Watch movies with subtitles in your language"
360
+ ],
361
+ 'B1': [
362
+ "πŸ“š Read intermediate articles on topics you enjoy",
363
+ "🎯 Practice expressing opinions and preferences",
364
+ "πŸ’‘ Start writing short paragraphs daily",
365
+ "⭐ Listen to podcasts at normal speed",
366
+ "πŸš€ Try to think in English for simple tasks"
367
+ ],
368
+ 'B2': [
369
+ "πŸ“š Read longer articles and opinion pieces",
370
+ "🎯 Practice formal and informal writing styles",
371
+ "πŸ’‘ Engage in debates and discussions",
372
+ "⭐ Watch news programs without subtitles",
373
+ "πŸš€ Set specific goals for each study session"
374
+ ],
375
+ 'C1': [
376
+ "πŸ“š Read academic and professional texts",
377
+ "🎯 Practice nuanced expressions and idioms",
378
+ "πŸ’‘ Write formal reports and presentations",
379
+ "⭐ Listen to academic lectures and conferences",
380
+ "πŸš€ Focus on specialized vocabulary for your field"
381
+ ],
382
+ 'C2': [
383
+ "πŸ“š Read literature and complex analytical texts",
384
+ "🎯 Master subtle language differences",
385
+ "πŸ’‘ Write with stylistic sophistication",
386
+ "⭐ Engage with native speakers in professional contexts",
387
+ "πŸš€ Aim for native-like fluency in all skills"
388
+ ]
389
+ }
390
+
391
+ return tips_by_level.get(level, tips_by_level['B1'])
392
+
393
+ def _create_milestones(self, current_level: str, target_level: str, weeks: int) -> List[Dict]:
394
+ """Create progress milestones"""
395
+ milestones = []
396
+
397
+ try:
398
+ milestone_intervals = max(2, weeks // 4) # Create 4 milestones
399
+
400
+ for i in range(1, 5):
401
+ week = milestone_intervals * i
402
+ if week <= weeks:
403
+ milestone = {
404
+ 'week': week,
405
+ 'title': f"Milestone {i}",
406
+ 'description': self._get_milestone_description(i, current_level, target_level),
407
+ 'target_date': (datetime.now() + timedelta(weeks=week)).date().isoformat(),
408
+ 'completed': False
409
+ }
410
+ milestones.append(milestone)
411
+
412
+ return milestones
413
+
414
+ except Exception as e:
415
+ logger.error(f"Error creating milestones: {e}")
416
+ return []
417
+
418
+ def _get_milestone_description(self, milestone_num: int, current_level: str, target_level: str) -> str:
419
+ """Get description for milestone"""
420
+ descriptions = {
421
+ 1: f"Complete foundation review and establish study routine",
422
+ 2: f"Reach intermediate proficiency between {current_level} and {target_level}",
423
+ 3: f"Demonstrate advanced skills approaching {target_level} level",
424
+ 4: f"Achieve {target_level} level proficiency in all skills"
425
+ }
426
+
427
+ return descriptions.get(milestone_num, f"Progress checkpoint {milestone_num}")
428
+
429
+ def _suggest_adaptations(self, user_data: Dict) -> List[str]:
430
+ """Suggest plan adaptations based on user data"""
431
+ adaptations = []
432
+
433
+ try:
434
+ weekly_hours = user_data.get('weekly_hours', 5)
435
+ context = user_data.get('context_focus', 'General/Social')
436
+ level = user_data.get('english_level', 'B1')
437
+
438
+ if weekly_hours < 4:
439
+ adaptations.append("πŸ’‘ Consider increasing study time to 4+ hours/week for faster progress")
440
+
441
+ if weekly_hours > 8:
442
+ adaptations.append("⚠️ Ensure you don't burn out - quality over quantity")
443
+
444
+ if context == 'Professional/Business':
445
+ adaptations.append("πŸ“Š Focus extra time on business writing and presentation skills")
446
+
447
+ if context == 'Technical/IT':
448
+ adaptations.append("πŸ’» Include technical documentation reading in your routine")
449
+
450
+ if level in ['C1', 'C2']:
451
+ adaptations.append("🎯 Consider specialized courses or certification preparation")
452
+
453
+ return adaptations[:3] # Limit to 3 adaptations
454
+
455
+ except Exception as e:
456
+ logger.error(f"Error suggesting adaptations: {e}")
457
+ return []
458
+
459
+ # Global instance
460
+ study_planner = StudyPlanner()