AIdeaText commited on
Commit
1fc131a
·
verified ·
1 Parent(s): a78887e

Upload 14 files

Browse files
modules/studentact/6-3-2025_current_situation_interface.py ADDED
@@ -0,0 +1,486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/studentact/current_situation_interface-vOK.py
2
+
3
+ import streamlit as st
4
+ import logging
5
+ from ..utils.widget_utils import generate_unique_key
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ from ..database.current_situation_mongo_db import store_current_situation_result
9
+
10
+ # Importaciones locales
11
+ from translations import get_translations
12
+
13
+ from .current_situation_analysis import (
14
+ analyze_text_dimensions,
15
+ analyze_clarity,
16
+ analyze_vocabulary_diversity,
17
+ analyze_cohesion,
18
+ analyze_structure,
19
+ get_dependency_depths,
20
+ normalize_score,
21
+ generate_sentence_graphs,
22
+ generate_word_connections,
23
+ generate_connection_paths,
24
+ create_vocabulary_network,
25
+ create_syntax_complexity_graph,
26
+ create_cohesion_heatmap,
27
+ generate_recommendations
28
+ )
29
+
30
+ # Configuración del estilo de matplotlib para el gráfico de radar
31
+ plt.rcParams['font.family'] = 'sans-serif'
32
+ plt.rcParams['axes.grid'] = True
33
+ plt.rcParams['axes.spines.top'] = False
34
+ plt.rcParams['axes.spines.right'] = False
35
+
36
+ logger = logging.getLogger(__name__)
37
+ ####################################
38
+
39
+ TEXT_TYPES = {
40
+ 'academic_article': {
41
+ 'name': 'Artículo Académico',
42
+ 'thresholds': {
43
+ 'vocabulary': {'min': 0.70, 'target': 0.85},
44
+ 'structure': {'min': 0.75, 'target': 0.90},
45
+ 'cohesion': {'min': 0.65, 'target': 0.80},
46
+ 'clarity': {'min': 0.70, 'target': 0.85}
47
+ }
48
+ },
49
+ 'student_essay': {
50
+ 'name': 'Trabajo Universitario',
51
+ 'thresholds': {
52
+ 'vocabulary': {'min': 0.60, 'target': 0.75},
53
+ 'structure': {'min': 0.65, 'target': 0.80},
54
+ 'cohesion': {'min': 0.55, 'target': 0.70},
55
+ 'clarity': {'min': 0.60, 'target': 0.75}
56
+ }
57
+ },
58
+ 'general_communication': {
59
+ 'name': 'Comunicación General',
60
+ 'thresholds': {
61
+ 'vocabulary': {'min': 0.50, 'target': 0.65},
62
+ 'structure': {'min': 0.55, 'target': 0.70},
63
+ 'cohesion': {'min': 0.45, 'target': 0.60},
64
+ 'clarity': {'min': 0.50, 'target': 0.65}
65
+ }
66
+ }
67
+ }
68
+ ####################################
69
+
70
+ def display_current_situation_interface(lang_code, nlp_models, t):
71
+ """
72
+ Interfaz simplificada con gráfico de radar para visualizar métricas.
73
+ """
74
+ # Inicializar estados si no existen
75
+ if 'text_input' not in st.session_state:
76
+ st.session_state.text_input = ""
77
+ if 'text_area' not in st.session_state: # Añadir inicialización de text_area
78
+ st.session_state.text_area = ""
79
+ if 'show_results' not in st.session_state:
80
+ st.session_state.show_results = False
81
+ if 'current_doc' not in st.session_state:
82
+ st.session_state.current_doc = None
83
+ if 'current_metrics' not in st.session_state:
84
+ st.session_state.current_metrics = None
85
+
86
+ try:
87
+ # Container principal con dos columnas
88
+ with st.container():
89
+ input_col, results_col = st.columns([1,2])
90
+
91
+ with input_col:
92
+ # Text area con manejo de estado
93
+ text_input = st.text_area(
94
+ t.get('input_prompt', "Escribe o pega tu texto aquí:"),
95
+ height=400,
96
+ key="text_area",
97
+ value=st.session_state.text_input,
98
+ help="Este texto será analizado para darte recomendaciones personalizadas"
99
+ )
100
+
101
+ # Función para manejar cambios de texto
102
+ if text_input != st.session_state.text_input:
103
+ st.session_state.text_input = text_input
104
+ st.session_state.show_results = False
105
+
106
+ if st.button(
107
+ t.get('analyze_button', "Analizar mi escritura"),
108
+ type="primary",
109
+ disabled=not text_input.strip(),
110
+ use_container_width=True,
111
+ ):
112
+ try:
113
+ with st.spinner(t.get('processing', "Analizando...")):
114
+ doc = nlp_models[lang_code](text_input)
115
+ metrics = analyze_text_dimensions(doc)
116
+
117
+ storage_success = store_current_situation_result(
118
+ username=st.session_state.username,
119
+ text=text_input,
120
+ metrics=metrics,
121
+ feedback=None
122
+ )
123
+
124
+ if not storage_success:
125
+ logger.warning("No se pudo guardar el análisis en la base de datos")
126
+
127
+ st.session_state.current_doc = doc
128
+ st.session_state.current_metrics = metrics
129
+ st.session_state.show_results = True
130
+
131
+ except Exception as e:
132
+ logger.error(f"Error en análisis: {str(e)}")
133
+ st.error(t.get('analysis_error', "Error al analizar el texto"))
134
+
135
+ # Mostrar resultados en la columna derecha
136
+ with results_col:
137
+ if st.session_state.show_results and st.session_state.current_metrics is not None:
138
+ # Primero los radio buttons para tipo de texto
139
+ st.markdown("### Tipo de texto")
140
+ text_type = st.radio(
141
+ "",
142
+ options=list(TEXT_TYPES.keys()),
143
+ format_func=lambda x: TEXT_TYPES[x]['name'],
144
+ horizontal=True,
145
+ key="text_type_radio",
146
+ help="Selecciona el tipo de texto para ajustar los criterios de evaluación"
147
+ )
148
+
149
+ st.session_state.current_text_type = text_type
150
+
151
+ # Luego mostrar los resultados
152
+ display_results(
153
+ metrics=st.session_state.current_metrics,
154
+ text_type=text_type
155
+ )
156
+
157
+ except Exception as e:
158
+ logger.error(f"Error en interfaz principal: {str(e)}")
159
+ st.error("Ocurrió un error al cargar la interfaz")
160
+
161
+ ###################################3333
162
+
163
+ '''
164
+ def display_results(metrics, text_type=None):
165
+ """
166
+ Muestra los resultados del análisis: métricas verticalmente y gráfico radar.
167
+ """
168
+ try:
169
+ # Usar valor por defecto si no se especifica tipo
170
+ text_type = text_type or 'student_essay'
171
+
172
+ # Obtener umbrales según el tipo de texto
173
+ thresholds = TEXT_TYPES[text_type]['thresholds']
174
+
175
+ # Crear dos columnas para las métricas y el gráfico
176
+ metrics_col, graph_col = st.columns([1, 1.5])
177
+
178
+ # Columna de métricas
179
+ with metrics_col:
180
+ metrics_config = [
181
+ {
182
+ 'label': "Vocabulario",
183
+ 'key': 'vocabulary',
184
+ 'value': metrics['vocabulary']['normalized_score'],
185
+ 'help': "Riqueza y variedad del vocabulario",
186
+ 'thresholds': thresholds['vocabulary']
187
+ },
188
+ {
189
+ 'label': "Estructura",
190
+ 'key': 'structure',
191
+ 'value': metrics['structure']['normalized_score'],
192
+ 'help': "Organización y complejidad de oraciones",
193
+ 'thresholds': thresholds['structure']
194
+ },
195
+ {
196
+ 'label': "Cohesión",
197
+ 'key': 'cohesion',
198
+ 'value': metrics['cohesion']['normalized_score'],
199
+ 'help': "Conexión y fluidez entre ideas",
200
+ 'thresholds': thresholds['cohesion']
201
+ },
202
+ {
203
+ 'label': "Claridad",
204
+ 'key': 'clarity',
205
+ 'value': metrics['clarity']['normalized_score'],
206
+ 'help': "Facilidad de comprensión del texto",
207
+ 'thresholds': thresholds['clarity']
208
+ }
209
+ ]
210
+
211
+ # Mostrar métricas
212
+ for metric in metrics_config:
213
+ value = metric['value']
214
+ if value < metric['thresholds']['min']:
215
+ status = "⚠️ Por mejorar"
216
+ color = "inverse"
217
+ elif value < metric['thresholds']['target']:
218
+ status = "📈 Aceptable"
219
+ color = "off"
220
+ else:
221
+ status = "✅ Óptimo"
222
+ color = "normal"
223
+
224
+ st.metric(
225
+ metric['label'],
226
+ f"{value:.2f}",
227
+ f"{status} (Meta: {metric['thresholds']['target']:.2f})",
228
+ delta_color=color,
229
+ help=metric['help']
230
+ )
231
+ st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
232
+
233
+ # Gráfico radar en la columna derecha
234
+ with graph_col:
235
+ display_radar_chart(metrics_config, thresholds)
236
+
237
+ except Exception as e:
238
+ logger.error(f"Error mostrando resultados: {str(e)}")
239
+ st.error("Error al mostrar los resultados")
240
+ '''
241
+
242
+ ######################################
243
+ ######################################
244
+ def display_results(metrics, text_type=None):
245
+ """
246
+ Muestra los resultados del análisis: métricas verticalmente y gráfico radar.
247
+ """
248
+ try:
249
+ # Usar valor por defecto si no se especifica tipo
250
+ text_type = text_type or 'student_essay'
251
+
252
+ # Obtener umbrales según el tipo de texto
253
+ thresholds = TEXT_TYPES[text_type]['thresholds']
254
+
255
+ # Crear dos columnas para las métricas y el gráfico
256
+ metrics_col, graph_col = st.columns([1, 1.5])
257
+
258
+ # Columna de métricas
259
+ with metrics_col:
260
+ metrics_config = [
261
+ {
262
+ 'label': "Vocabulario",
263
+ 'key': 'vocabulary',
264
+ 'value': metrics['vocabulary']['normalized_score'],
265
+ 'help': "Riqueza y variedad del vocabulario",
266
+ 'thresholds': thresholds['vocabulary']
267
+ },
268
+ {
269
+ 'label': "Estructura",
270
+ 'key': 'structure',
271
+ 'value': metrics['structure']['normalized_score'],
272
+ 'help': "Organización y complejidad de oraciones",
273
+ 'thresholds': thresholds['structure']
274
+ },
275
+ {
276
+ 'label': "Cohesión",
277
+ 'key': 'cohesion',
278
+ 'value': metrics['cohesion']['normalized_score'],
279
+ 'help': "Conexión y fluidez entre ideas",
280
+ 'thresholds': thresholds['cohesion']
281
+ },
282
+ {
283
+ 'label': "Claridad",
284
+ 'key': 'clarity',
285
+ 'value': metrics['clarity']['normalized_score'],
286
+ 'help': "Facilidad de comprensión del texto",
287
+ 'thresholds': thresholds['clarity']
288
+ }
289
+ ]
290
+
291
+ # Mostrar métricas
292
+ for metric in metrics_config:
293
+ value = metric['value']
294
+ if value < metric['thresholds']['min']:
295
+ status = "⚠️ Por mejorar"
296
+ color = "inverse"
297
+ elif value < metric['thresholds']['target']:
298
+ status = "📈 Aceptable"
299
+ color = "off"
300
+ else:
301
+ status = "✅ Óptimo"
302
+ color = "normal"
303
+
304
+ st.metric(
305
+ metric['label'],
306
+ f"{value:.2f}",
307
+ f"{status} (Meta: {metric['thresholds']['target']:.2f})",
308
+ delta_color=color,
309
+ help=metric['help']
310
+ )
311
+ st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
312
+
313
+ # Gráfico radar en la columna derecha
314
+ with graph_col:
315
+ display_radar_chart(metrics_config, thresholds)
316
+
317
+ recommendations = generate_recommendations(
318
+ metrics=metrics,
319
+ text_type=text_type,
320
+ lang_code=st.session_state.lang_code
321
+ )
322
+
323
+ # Separador visual
324
+ st.markdown("---")
325
+
326
+ # Título para la sección de recomendaciones
327
+ st.subheader("Recomendaciones para mejorar tu escritura")
328
+
329
+ # Mostrar las recomendaciones
330
+ display_recommendations(recommendations, get_translations(st.session_state.lang_code))
331
+
332
+ except Exception as e:
333
+ logger.error(f"Error mostrando resultados: {str(e)}")
334
+ st.error("Error al mostrar los resultados")
335
+
336
+
337
+ ######################################
338
+ ######################################
339
+ def display_radar_chart(metrics_config, thresholds):
340
+ """
341
+ Muestra el gráfico radar con los resultados.
342
+ """
343
+ try:
344
+ # Preparar datos para el gráfico
345
+ categories = [m['label'] for m in metrics_config]
346
+ values_user = [m['value'] for m in metrics_config]
347
+ min_values = [m['thresholds']['min'] for m in metrics_config]
348
+ target_values = [m['thresholds']['target'] for m in metrics_config]
349
+
350
+ # Crear y configurar gráfico
351
+ fig = plt.figure(figsize=(8, 8))
352
+ ax = fig.add_subplot(111, projection='polar')
353
+
354
+ # Configurar radar
355
+ angles = [n / float(len(categories)) * 2 * np.pi for n in range(len(categories))]
356
+ angles += angles[:1]
357
+ values_user += values_user[:1]
358
+ min_values += min_values[:1]
359
+ target_values += target_values[:1]
360
+
361
+ # Configurar ejes
362
+ ax.set_xticks(angles[:-1])
363
+ ax.set_xticklabels(categories, fontsize=10)
364
+ circle_ticks = np.arange(0, 1.1, 0.2)
365
+ ax.set_yticks(circle_ticks)
366
+ ax.set_yticklabels([f'{tick:.1f}' for tick in circle_ticks], fontsize=8)
367
+ ax.set_ylim(0, 1)
368
+
369
+ # Dibujar áreas de umbrales
370
+ ax.plot(angles, min_values, '#e74c3c', linestyle='--', linewidth=1, label='Mínimo', alpha=0.5)
371
+ ax.plot(angles, target_values, '#2ecc71', linestyle='--', linewidth=1, label='Meta', alpha=0.5)
372
+ ax.fill_between(angles, target_values, [1]*len(angles), color='#2ecc71', alpha=0.1)
373
+ ax.fill_between(angles, [0]*len(angles), min_values, color='#e74c3c', alpha=0.1)
374
+
375
+ # Dibujar valores del usuario
376
+ ax.plot(angles, values_user, '#3498db', linewidth=2, label='Tu escritura')
377
+ ax.fill(angles, values_user, '#3498db', alpha=0.2)
378
+
379
+ # Ajustar leyenda
380
+ ax.legend(
381
+ loc='upper right',
382
+ bbox_to_anchor=(1.3, 1.1), # Cambiado de (0.1, 0.1) a (1.3, 1.1)
383
+ fontsize=10,
384
+ frameon=True,
385
+ facecolor='white',
386
+ edgecolor='none',
387
+ shadow=True
388
+ )
389
+
390
+ plt.tight_layout()
391
+ st.pyplot(fig)
392
+ plt.close()
393
+
394
+ except Exception as e:
395
+ logger.error(f"Error mostrando gráfico radar: {str(e)}")
396
+ st.error("Error al mostrar el gráfico")
397
+
398
+ #####################################################
399
+ def display_recommendations(recommendations, t):
400
+ """
401
+ Muestra las recomendaciones con un diseño de tarjetas.
402
+ """
403
+ # Definir colores para cada categoría
404
+ colors = {
405
+ 'vocabulary': '#2E86C1', # Azul
406
+ 'structure': '#28B463', # Verde
407
+ 'cohesion': '#F39C12', # Naranja
408
+ 'clarity': '#9B59B6', # Púrpura
409
+ 'priority': '#E74C3C' # Rojo para la categoría prioritaria
410
+ }
411
+
412
+ # Iconos para cada categoría
413
+ icons = {
414
+ 'vocabulary': '📚',
415
+ 'structure': '🏗️',
416
+ 'cohesion': '🔄',
417
+ 'clarity': '💡',
418
+ 'priority': '⭐'
419
+ }
420
+
421
+ # Obtener traducciones para cada dimensión
422
+ dimension_names = {
423
+ 'vocabulary': t.get('SITUATION_ANALYSIS', {}).get('vocabulary', "Vocabulario"),
424
+ 'structure': t.get('SITUATION_ANALYSIS', {}).get('structure', "Estructura"),
425
+ 'cohesion': t.get('SITUATION_ANALYSIS', {}).get('cohesion', "Cohesión"),
426
+ 'clarity': t.get('SITUATION_ANALYSIS', {}).get('clarity', "Claridad"),
427
+ 'priority': t.get('SITUATION_ANALYSIS', {}).get('priority', "Prioridad")
428
+ }
429
+
430
+ # Título de la sección prioritaria
431
+ priority_focus = t.get('SITUATION_ANALYSIS', {}).get('priority_focus', 'Área prioritaria para mejorar')
432
+ st.markdown(f"### {icons['priority']} {priority_focus}")
433
+
434
+ # Determinar área prioritaria (la que tiene menor puntuación)
435
+ priority_area = recommendations.get('priority', 'vocabulary')
436
+ priority_title = dimension_names.get(priority_area, "Área prioritaria")
437
+
438
+ # Determinar el contenido para mostrar
439
+ if isinstance(recommendations[priority_area], dict) and 'title' in recommendations[priority_area]:
440
+ priority_title = recommendations[priority_area]['title']
441
+ priority_content = recommendations[priority_area]['content']
442
+ else:
443
+ priority_content = recommendations[priority_area]
444
+
445
+ # Mostrar la recomendación prioritaria con un estilo destacado
446
+ with st.container():
447
+ st.markdown(
448
+ f"""
449
+ <div style="border:2px solid {colors['priority']}; border-radius:5px; padding:15px; margin-bottom:20px;">
450
+ <h4 style="color:{colors['priority']};">{priority_title}</h4>
451
+ <p>{priority_content}</p>
452
+ </div>
453
+ """,
454
+ unsafe_allow_html=True
455
+ )
456
+
457
+ # Crear dos columnas para las tarjetas de recomendaciones restantes
458
+ col1, col2 = st.columns(2)
459
+
460
+ # Distribuir las recomendaciones en las columnas
461
+ categories = ['vocabulary', 'structure', 'cohesion', 'clarity']
462
+ for i, category in enumerate(categories):
463
+ # Saltar si esta categoría ya es la prioritaria
464
+ if category == priority_area:
465
+ continue
466
+
467
+ # Determinar título y contenido
468
+ if isinstance(recommendations[category], dict) and 'title' in recommendations[category]:
469
+ category_title = recommendations[category]['title']
470
+ category_content = recommendations[category]['content']
471
+ else:
472
+ category_title = dimension_names.get(category, category)
473
+ category_content = recommendations[category]
474
+
475
+ # Alternar entre columnas
476
+ with col1 if i % 2 == 0 else col2:
477
+ # Crear tarjeta para cada recomendación
478
+ st.markdown(
479
+ f"""
480
+ <div style="border:1px solid {colors[category]}; border-radius:5px; padding:10px; margin-bottom:15px;">
481
+ <h4 style="color:{colors[category]};">{icons[category]} {category_title}</h4>
482
+ <p>{category_content}</p>
483
+ </div>
484
+ """,
485
+ unsafe_allow_html=True
486
+ )
modules/studentact/claude_recommendations.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/studentact/claude_recommendations.py
2
+ import os
3
+ import anthropic
4
+ import streamlit as st
5
+ import logging
6
+ import time
7
+ import json
8
+ from datetime import datetime, timezone
9
+
10
+ # Local imports
11
+ from ..utils.widget_utils import generate_unique_key
12
+ from ..database.current_situation_mongo_db import store_current_situation_result
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Define text types
17
+ TEXT_TYPES = {
18
+ 'es': {
19
+ 'academic_article': 'artículo académico',
20
+ 'university_work': 'trabajo universitario',
21
+ 'general_communication': 'comunicación general'
22
+ },
23
+ 'en': {
24
+ 'academic_article': 'academic article',
25
+ 'university_work': 'university work',
26
+ 'general_communication': 'general communication'
27
+ },
28
+ 'fr': {
29
+ 'academic_article': 'article académique',
30
+ 'university_work': 'travail universitaire',
31
+ 'general_communication': 'communication générale'
32
+ }
33
+ }
34
+
35
+ # Cache for recommendations to avoid redundant API calls
36
+ recommendation_cache = {}
37
+
38
+ def get_recommendation_cache_key(text, metrics, text_type, lang_code):
39
+ """
40
+ Generate a cache key for recommendations.
41
+ """
42
+ # Create a simple hash based on text content and metrics
43
+ text_hash = hash(text[:1000]) # Only use first 1000 chars for hashing
44
+ metrics_hash = hash(json.dumps(metrics, sort_keys=True))
45
+ return f"{text_hash}_{metrics_hash}_{text_type}_{lang_code}"
46
+
47
+ def format_metrics_for_claude(metrics, lang_code, text_type):
48
+ """
49
+ Format metrics in a way that's readable for Claude
50
+ """
51
+ formatted_metrics = {}
52
+ for key, value in metrics.items():
53
+ if isinstance(value, (int, float)):
54
+ formatted_metrics[key] = round(value, 2)
55
+ else:
56
+ formatted_metrics[key] = value
57
+
58
+ # Add context about what type of text this is
59
+ text_type_label = TEXT_TYPES.get(lang_code, {}).get(text_type, text_type)
60
+ formatted_metrics['text_type'] = text_type_label
61
+
62
+ return formatted_metrics
63
+
64
+ def generate_claude_recommendations(text, metrics, text_type, lang_code):
65
+ """
66
+ Generate personalized recommendations using Claude API.
67
+ """
68
+ try:
69
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
70
+ if not api_key:
71
+ logger.error("Claude API key not found in environment variables")
72
+ return get_fallback_recommendations(lang_code)
73
+
74
+ # Check cache first
75
+ cache_key = get_recommendation_cache_key(text, metrics, text_type, lang_code)
76
+ if cache_key in recommendation_cache:
77
+ logger.info("Using cached recommendations")
78
+ return recommendation_cache[cache_key]
79
+
80
+ # Format metrics for Claude
81
+ formatted_metrics = format_metrics_for_claude(metrics, lang_code, text_type)
82
+
83
+ # Determine language for prompt
84
+ if lang_code == 'es':
85
+ system_prompt = """Eres un asistente especializado en análisis de textos académicos y comunicación escrita.
86
+ Tu tarea es analizar el texto del usuario y proporcionar recomendaciones personalizadas.
87
+ Usa un tono constructivo y específico. Sé claro y directo con tus sugerencias.
88
+ """
89
+ user_prompt = f"""Por favor, analiza este texto de tipo '{formatted_metrics['text_type']}'
90
+ y proporciona recomendaciones personalizadas para mejorarlo.
91
+
92
+ MÉTRICAS DE ANÁLISIS:
93
+ {json.dumps(formatted_metrics, indent=2, ensure_ascii=False)}
94
+
95
+ TEXTO A ANALIZAR:
96
+ {text[:2000]} # Limitamos el texto para evitar exceder tokens
97
+
98
+ Proporciona tu análisis con el siguiente formato:
99
+ 1. Un resumen breve (2-3 frases) del análisis general
100
+ 2. 3-4 recomendaciones específicas y accionables (cada una de 1-2 frases)
101
+ 3. Un ejemplo concreto de mejora tomado del propio texto del usuario
102
+ 4. Una sugerencia sobre qué herramienta de AIdeaText usar (Análisis Morfosintáctico, Análisis Semántico o Análisis del Discurso)
103
+
104
+ Tu respuesta debe ser concisa y no exceder los 300 palabras."""
105
+ else:
106
+ # Default to English
107
+ system_prompt = """You are an assistant specialized in analyzing academic texts and written communication.
108
+ Your task is to analyze the user's text and provide personalized recommendations.
109
+ Use a constructive and specific tone. Be clear and direct with your suggestions.
110
+ """
111
+ user_prompt = f"""Please analyze this text of type '{formatted_metrics['text_type']}'
112
+ and provide personalized recommendations to improve it.
113
+
114
+ ANALYSIS METRICS:
115
+ {json.dumps(formatted_metrics, indent=2, ensure_ascii=False)}
116
+
117
+ TEXT TO ANALYZE:
118
+ {text[:2000]} # Limiting text to avoid exceeding tokens
119
+
120
+ Provide your analysis with the following format:
121
+ 1. A brief summary (2-3 sentences) of the general analysis
122
+ 2. 3-4 specific and actionable recommendations (each 1-2 sentences)
123
+ 3. A concrete example of improvement taken from the user's own text
124
+ 4. A suggestion about which AIdeaText tool to use (Morphosyntactic Analysis, Semantic Analysis or Discourse Analysis)
125
+
126
+ Your response should be concise and not exceed 300 words."""
127
+
128
+ # Initialize Claude client
129
+ client = anthropic.Anthropic(api_key=api_key)
130
+
131
+ # Call Claude API
132
+ start_time = time.time()
133
+ response = client.messages.create(
134
+ model="claude-3-5-sonnet-20241022",
135
+ max_tokens=1024,
136
+ temperature=0.7,
137
+ system=system_prompt,
138
+ messages=[
139
+ {"role": "user", "content": user_prompt}
140
+ ]
141
+ )
142
+ logger.info(f"Claude API call completed in {time.time() - start_time:.2f} seconds")
143
+
144
+ # Extract recommendations
145
+ recommendations = response.content[0].text
146
+
147
+ # Cache the result
148
+ recommendation_cache[cache_key] = recommendations
149
+
150
+ return recommendations
151
+ except Exception as e:
152
+ logger.error(f"Error generating recommendations with Claude: {str(e)}")
153
+ return get_fallback_recommendations(lang_code)
154
+
155
+ def get_fallback_recommendations(lang_code):
156
+ """
157
+ Return fallback recommendations if Claude API fails
158
+ """
159
+ if lang_code == 'es':
160
+ return """
161
+ **Análisis General**
162
+ Tu texto presenta una estructura básica adecuada, pero hay áreas que pueden mejorarse para mayor claridad y cohesión.
163
+
164
+ **Recomendaciones**:
165
+ - Intenta variar tu vocabulario para evitar repeticiones innecesarias
166
+ - Considera revisar la longitud de tus oraciones para mantener un mejor ritmo
167
+ - Asegúrate de establecer conexiones claras entre las ideas principales
168
+ - Revisa la consistencia en el uso de tiempos verbales
169
+
170
+ **Herramienta recomendada**:
171
+ Te sugerimos utilizar el Análisis Morfosintáctico para identificar patrones en tu estructura de oraciones.
172
+ """
173
+ else:
174
+ return """
175
+ **General Analysis**
176
+ Your text presents an adequate basic structure, but there are areas that can be improved for better clarity and cohesion.
177
+
178
+ **Recommendations**:
179
+ - Try to vary your vocabulary to avoid unnecessary repetition
180
+ - Consider reviewing the length of your sentences to maintain a better rhythm
181
+ - Make sure to establish clear connections between main ideas
182
+ - Check consistency in the use of verb tenses
183
+
184
+ **Recommended tool**:
185
+ We suggest using Morphosyntactic Analysis to identify patterns in your sentence structure.
186
+ """
187
+
188
+
189
+ #######################################
190
+
191
+ def store_recommendations(username, text, metrics, text_type, recommendations):
192
+ """
193
+ Store the recommendations in the database
194
+ """
195
+ try:
196
+ # Importar la función de almacenamiento de recomendaciones
197
+ from ..database.claude_recommendations_mongo_db import store_claude_recommendation
198
+
199
+ # Guardar usando la nueva función especializada
200
+ result = store_claude_recommendation(
201
+ username=username,
202
+ text=text,
203
+ metrics=metrics,
204
+ text_type=text_type,
205
+ recommendations=recommendations
206
+ )
207
+
208
+ logger.info(f"Recommendations stored successfully: {result}")
209
+ return result
210
+ except Exception as e:
211
+ logger.error(f"Error storing recommendations: {str(e)}")
212
+ return False
213
+
214
+
215
+ ##########################################
216
+ ##########################################
217
+ def display_personalized_recommendations(text, metrics, text_type, lang_code, t):
218
+ """
219
+ Display personalized recommendations based on text analysis
220
+ """
221
+ try:
222
+ # Generate recommendations
223
+ recommendations = generate_claude_recommendations(text, metrics, text_type, lang_code)
224
+
225
+ # Format and display recommendations in a nice container
226
+ st.markdown("### 📝 " + t.get('recommendations_title', 'Personalized Recommendations'))
227
+
228
+ with st.container():
229
+ st.markdown(f"""
230
+ <div style="padding: 20px; border-radius: 10px;
231
+ background-color: #f8f9fa; margin-bottom: 20px;">
232
+ {recommendations}
233
+ </div>
234
+ """, unsafe_allow_html=True)
235
+
236
+ # Add prompt to use assistant
237
+ st.info("💡 **" + t.get('assistant_prompt', 'For further improvement:') + "** " +
238
+ t.get('assistant_message', 'Open the virtual assistant (powered by Claude AI) in the upper left corner by clicking the arrow next to the logo.'))
239
+
240
+ # Add save button
241
+ col1, col2, col3 = st.columns([1,1,1])
242
+ with col2:
243
+ if st.button(
244
+ t.get('save_button', 'Save Analysis'),
245
+ key=generate_unique_key("claude_recommendations", "save"),
246
+ type="primary",
247
+ use_container_width=True
248
+ ):
249
+ if 'username' in st.session_state:
250
+ success = store_recommendations(
251
+ st.session_state.username,
252
+ text,
253
+ metrics,
254
+ text_type,
255
+ recommendations
256
+ )
257
+ if success:
258
+ st.success(t.get('save_success', 'Analysis saved successfully'))
259
+ else:
260
+ st.error(t.get('save_error', 'Error saving analysis'))
261
+ else:
262
+ st.error(t.get('login_required', 'Please log in to save analysis'))
263
+
264
+ except Exception as e:
265
+ logger.error(f"Error displaying recommendations: {str(e)}")
266
+ st.error(t.get('recommendations_error', 'Error generating recommendations. Please try again later.'))
modules/studentact/current_situation_analysis-FAIL.py CHANGED
@@ -1,810 +1,810 @@
1
- #v3/modules/studentact/current_situation_analysis.py
2
-
3
- import streamlit as st
4
- import matplotlib.pyplot as plt
5
- import networkx as nx
6
- import seaborn as sns
7
- from collections import Counter
8
- from itertools import combinations
9
- import numpy as np
10
- import matplotlib.patches as patches
11
- import logging
12
-
13
- # 2. Configuración básica del logging
14
- logging.basicConfig(
15
- level=logging.INFO,
16
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
17
- handlers=[
18
- logging.StreamHandler(),
19
- logging.FileHandler('app.log')
20
- ]
21
- )
22
-
23
- # 3. Obtener el logger específico para este módulo
24
- logger = logging.getLogger(__name__)
25
-
26
- #########################################################################
27
-
28
- def correlate_metrics(scores):
29
- """
30
- Ajusta los scores para mantener correlaciones lógicas entre métricas.
31
-
32
- Args:
33
- scores: dict con scores iniciales de vocabulario, estructura, cohesión y claridad
34
-
35
- Returns:
36
- dict con scores ajustados
37
- """
38
- try:
39
- # 1. Correlación estructura-cohesión
40
- # La cohesión no puede ser menor que estructura * 0.7
41
- min_cohesion = scores['structure']['normalized_score'] * 0.7
42
- if scores['cohesion']['normalized_score'] < min_cohesion:
43
- scores['cohesion']['normalized_score'] = min_cohesion
44
-
45
- # 2. Correlación vocabulario-cohesión
46
- # La cohesión léxica depende del vocabulario
47
- vocab_influence = scores['vocabulary']['normalized_score'] * 0.6
48
- scores['cohesion']['normalized_score'] = max(
49
- scores['cohesion']['normalized_score'],
50
- vocab_influence
51
- )
52
-
53
- # 3. Correlación cohesión-claridad
54
- # La claridad no puede superar cohesión * 1.2
55
- max_clarity = scores['cohesion']['normalized_score'] * 1.2
56
- if scores['clarity']['normalized_score'] > max_clarity:
57
- scores['clarity']['normalized_score'] = max_clarity
58
-
59
- # 4. Correlación estructura-claridad
60
- # La claridad no puede superar estructura * 1.1
61
- struct_max_clarity = scores['structure']['normalized_score'] * 1.1
62
- scores['clarity']['normalized_score'] = min(
63
- scores['clarity']['normalized_score'],
64
- struct_max_clarity
65
- )
66
-
67
- # Normalizar todos los scores entre 0 y 1
68
- for metric in scores:
69
- scores[metric]['normalized_score'] = max(0.0, min(1.0, scores[metric]['normalized_score']))
70
-
71
- return scores
72
-
73
- except Exception as e:
74
- logger.error(f"Error en correlate_metrics: {str(e)}")
75
- return scores
76
-
77
- ##########################################################################
78
-
79
- def analyze_text_dimensions(doc):
80
- """
81
- Analiza las dimensiones principales del texto manteniendo correlaciones lógicas.
82
- """
83
- try:
84
- # Obtener scores iniciales
85
- vocab_score, vocab_details = analyze_vocabulary_diversity(doc)
86
- struct_score = analyze_structure(doc)
87
- cohesion_score = analyze_cohesion(doc)
88
- clarity_score, clarity_details = analyze_clarity(doc)
89
-
90
- # Crear diccionario de scores inicial
91
- scores = {
92
- 'vocabulary': {
93
- 'normalized_score': vocab_score,
94
- 'details': vocab_details
95
- },
96
- 'structure': {
97
- 'normalized_score': struct_score,
98
- 'details': None
99
- },
100
- 'cohesion': {
101
- 'normalized_score': cohesion_score,
102
- 'details': None
103
- },
104
- 'clarity': {
105
- 'normalized_score': clarity_score,
106
- 'details': clarity_details
107
- }
108
- }
109
-
110
- # Ajustar correlaciones entre métricas
111
- adjusted_scores = correlate_metrics(scores)
112
-
113
- # Logging para diagnóstico
114
- logger.info(f"""
115
- Scores originales vs ajustados:
116
- Vocabulario: {vocab_score:.2f} -> {adjusted_scores['vocabulary']['normalized_score']:.2f}
117
- Estructura: {struct_score:.2f} -> {adjusted_scores['structure']['normalized_score']:.2f}
118
- Cohesión: {cohesion_score:.2f} -> {adjusted_scores['cohesion']['normalized_score']:.2f}
119
- Claridad: {clarity_score:.2f} -> {adjusted_scores['clarity']['normalized_score']:.2f}
120
- """)
121
-
122
- return adjusted_scores
123
-
124
- except Exception as e:
125
- logger.error(f"Error en analyze_text_dimensions: {str(e)}")
126
- return {
127
- 'vocabulary': {'normalized_score': 0.0, 'details': {}},
128
- 'structure': {'normalized_score': 0.0, 'details': {}},
129
- 'cohesion': {'normalized_score': 0.0, 'details': {}},
130
- 'clarity': {'normalized_score': 0.0, 'details': {}}
131
- }
132
-
133
-
134
-
135
- #############################################################################################
136
-
137
- def analyze_clarity(doc):
138
- """
139
- Analiza la claridad del texto considerando múltiples factores.
140
- """
141
- try:
142
- sentences = list(doc.sents)
143
- if not sentences:
144
- return 0.0, {}
145
-
146
- # 1. Longitud de oraciones
147
- sentence_lengths = [len(sent) for sent in sentences]
148
- avg_length = sum(sentence_lengths) / len(sentences)
149
-
150
- # Normalizar usando los umbrales definidos para clarity
151
- length_score = normalize_score(
152
- value=avg_length,
153
- metric_type='clarity',
154
- optimal_length=20, # Una oración ideal tiene ~20 palabras
155
- min_threshold=0.60, # Consistente con METRIC_THRESHOLDS
156
- target_threshold=0.75 # Consistente con METRIC_THRESHOLDS
157
- )
158
-
159
- # 2. Análisis de conectores
160
- connector_count = 0
161
- connector_weights = {
162
- 'CCONJ': 1.0, # Coordinantes
163
- 'SCONJ': 1.2, # Subordinantes
164
- 'ADV': 0.8 # Adverbios conectivos
165
- }
166
-
167
- for token in doc:
168
- if token.pos_ in connector_weights and token.dep_ in ['cc', 'mark', 'advmod']:
169
- connector_count += connector_weights[token.pos_]
170
-
171
- # Normalizar conectores por oración
172
- connectors_per_sentence = connector_count / len(sentences) if sentences else 0
173
- connector_score = normalize_score(
174
- value=connectors_per_sentence,
175
- metric_type='clarity',
176
- optimal_connections=1.5, # ~1.5 conectores por oración es óptimo
177
- min_threshold=0.60,
178
- target_threshold=0.75
179
- )
180
-
181
- # 3. Complejidad estructural
182
- clause_count = 0
183
- for sent in sentences:
184
- verbs = [token for token in sent if token.pos_ == 'VERB']
185
- clause_count += len(verbs)
186
-
187
- complexity_raw = clause_count / len(sentences) if sentences else 0
188
- complexity_score = normalize_score(
189
- value=complexity_raw,
190
- metric_type='clarity',
191
- optimal_depth=2.0, # ~2 cláusulas por oración es óptimo
192
- min_threshold=0.60,
193
- target_threshold=0.75
194
- )
195
-
196
- # 4. Densidad léxica
197
- content_words = len([token for token in doc if token.pos_ in ['NOUN', 'VERB', 'ADJ', 'ADV']])
198
- total_words = len([token for token in doc if token.is_alpha])
199
- density = content_words / total_words if total_words > 0 else 0
200
-
201
- density_score = normalize_score(
202
- value=density,
203
- metric_type='clarity',
204
- optimal_connections=0.6, # 60% de palabras de contenido es óptimo
205
- min_threshold=0.60,
206
- target_threshold=0.75
207
- )
208
-
209
- # Score final ponderado
210
- weights = {
211
- 'length': 0.3,
212
- 'connectors': 0.3,
213
- 'complexity': 0.2,
214
- 'density': 0.2
215
- }
216
-
217
- clarity_score = (
218
- weights['length'] * length_score +
219
- weights['connectors'] * connector_score +
220
- weights['complexity'] * complexity_score +
221
- weights['density'] * density_score
222
- )
223
-
224
- details = {
225
- 'length_score': length_score,
226
- 'connector_score': connector_score,
227
- 'complexity_score': complexity_score,
228
- 'density_score': density_score,
229
- 'avg_sentence_length': avg_length,
230
- 'connectors_per_sentence': connectors_per_sentence,
231
- 'density': density
232
- }
233
-
234
- # Agregar logging para diagnóstico
235
- logger.info(f"""
236
- Scores de Claridad:
237
- - Longitud: {length_score:.2f} (avg={avg_length:.1f} palabras)
238
- - Conectores: {connector_score:.2f} (avg={connectors_per_sentence:.1f} por oración)
239
- - Complejidad: {complexity_score:.2f} (avg={complexity_raw:.1f} cláusulas)
240
- - Densidad: {density_score:.2f} ({density*100:.1f}% palabras de contenido)
241
- - Score Final: {clarity_score:.2f}
242
- """)
243
-
244
- return clarity_score, details
245
-
246
- except Exception as e:
247
- logger.error(f"Error en analyze_clarity: {str(e)}")
248
- return 0.0, {}
249
-
250
-
251
- def analyze_vocabulary_diversity(doc):
252
- """Análisis mejorado de la diversidad y calidad del vocabulario"""
253
- try:
254
- # 1. Análisis básico de diversidad
255
- unique_lemmas = {token.lemma_ for token in doc if token.is_alpha}
256
- total_words = len([token for token in doc if token.is_alpha])
257
- basic_diversity = len(unique_lemmas) / total_words if total_words > 0 else 0
258
-
259
- # 2. Análisis de registro
260
- academic_words = 0
261
- narrative_words = 0
262
- technical_terms = 0
263
-
264
- # Clasificar palabras por registro
265
- for token in doc:
266
- if token.is_alpha:
267
- # Detectar términos académicos/técnicos
268
- if token.pos_ in ['NOUN', 'VERB', 'ADJ']:
269
- if any(parent.pos_ == 'NOUN' for parent in token.ancestors):
270
- technical_terms += 1
271
- # Detectar palabras narrativas
272
- if token.pos_ in ['VERB', 'ADV'] and token.dep_ in ['ROOT', 'advcl']:
273
- narrative_words += 1
274
-
275
- # 3. Análisis de complejidad sintáctica
276
- avg_sentence_length = sum(len(sent) for sent in doc.sents) / len(list(doc.sents))
277
-
278
- # 4. Calcular score ponderado
279
- weights = {
280
- 'diversity': 0.3,
281
- 'technical': 0.3,
282
- 'narrative': 0.2,
283
- 'complexity': 0.2
284
- }
285
-
286
- scores = {
287
- 'diversity': basic_diversity,
288
- 'technical': technical_terms / total_words if total_words > 0 else 0,
289
- 'narrative': narrative_words / total_words if total_words > 0 else 0,
290
- 'complexity': min(1.0, avg_sentence_length / 20) # Normalizado a 20 palabras
291
- }
292
-
293
- # Score final ponderado
294
- final_score = sum(weights[key] * scores[key] for key in weights)
295
-
296
- # Información adicional para diagnóstico
297
- details = {
298
- 'text_type': 'narrative' if scores['narrative'] > scores['technical'] else 'academic',
299
- 'scores': scores
300
- }
301
-
302
- return final_score, details
303
-
304
- except Exception as e:
305
- logger.error(f"Error en analyze_vocabulary_diversity: {str(e)}")
306
- return 0.0, {}
307
-
308
- def analyze_cohesion(doc):
309
- """Analiza la cohesión textual"""
310
- try:
311
- sentences = list(doc.sents)
312
- if len(sentences) < 2:
313
- logger.warning("Texto demasiado corto para análisis de cohesión")
314
- return 0.0
315
-
316
- # 1. Análisis de conexiones léxicas
317
- lexical_connections = 0
318
- total_possible_connections = 0
319
-
320
- for i in range(len(sentences)-1):
321
- # Obtener lemmas significativos (no stopwords)
322
- sent1_words = {token.lemma_ for token in sentences[i]
323
- if token.is_alpha and not token.is_stop}
324
- sent2_words = {token.lemma_ for token in sentences[i+1]
325
- if token.is_alpha and not token.is_stop}
326
-
327
- if sent1_words and sent2_words: # Verificar que ambos conjuntos no estén vacíos
328
- intersection = len(sent1_words.intersection(sent2_words))
329
- total_possible = min(len(sent1_words), len(sent2_words))
330
-
331
- if total_possible > 0:
332
- lexical_score = intersection / total_possible
333
- lexical_connections += lexical_score
334
- total_possible_connections += 1
335
-
336
- # 2. Análisis de conectores
337
- connector_count = 0
338
- connector_types = {
339
- 'CCONJ': 1.0, # Coordinantes
340
- 'SCONJ': 1.2, # Subordinantes
341
- 'ADV': 0.8 # Adverbios conectivos
342
- }
343
-
344
- for token in doc:
345
- if (token.pos_ in connector_types and
346
- token.dep_ in ['cc', 'mark', 'advmod'] and
347
- not token.is_stop):
348
- connector_count += connector_types[token.pos_]
349
-
350
- # 3. Cálculo de scores normalizados
351
- if total_possible_connections > 0:
352
- lexical_cohesion = lexical_connections / total_possible_connections
353
- else:
354
- lexical_cohesion = 0
355
-
356
- if len(sentences) > 1:
357
- connector_cohesion = min(1.0, connector_count / (len(sentences) - 1))
358
- else:
359
- connector_cohesion = 0
360
-
361
- # 4. Score final ponderado
362
- weights = {
363
- 'lexical': 0.7,
364
- 'connectors': 0.3
365
- }
366
-
367
- cohesion_score = (
368
- weights['lexical'] * lexical_cohesion +
369
- weights['connectors'] * connector_cohesion
370
- )
371
-
372
- # 5. Logging para diagnóstico
373
- logger.info(f"""
374
- Análisis de Cohesión:
375
- - Conexiones léxicas encontradas: {lexical_connections}
376
- - Conexiones posibles: {total_possible_connections}
377
- - Lexical cohesion score: {lexical_cohesion}
378
- - Conectores encontrados: {connector_count}
379
- - Connector cohesion score: {connector_cohesion}
380
- - Score final: {cohesion_score}
381
- """)
382
-
383
- return cohesion_score
384
-
385
- except Exception as e:
386
- logger.error(f"Error en analyze_cohesion: {str(e)}")
387
- return 0.0
388
-
389
- def analyze_structure(doc):
390
- try:
391
- if len(doc) == 0:
392
- return 0.0
393
-
394
- structure_scores = []
395
- for token in doc:
396
- if token.dep_ == 'ROOT':
397
- result = get_dependency_depths(token)
398
- structure_scores.append(result['final_score'])
399
-
400
- if not structure_scores:
401
- return 0.0
402
-
403
- return min(1.0, sum(structure_scores) / len(structure_scores))
404
-
405
- except Exception as e:
406
- logger.error(f"Error en analyze_structure: {str(e)}")
407
- return 0.0
408
-
409
- # Funciones auxiliares de análisis
410
-
411
- def get_dependency_depths(token, depth=0, analyzed_tokens=None):
412
- """
413
- Analiza la profundidad y calidad de las relaciones de dependencia.
414
-
415
- Args:
416
- token: Token a analizar
417
- depth: Profundidad actual en el árbol
418
- analyzed_tokens: Set para evitar ciclos en el análisis
419
-
420
- Returns:
421
- dict: Información detallada sobre las dependencias
422
- - depths: Lista de profundidades
423
- - relations: Diccionario con tipos de relaciones encontradas
424
- - complexity_score: Puntuación de complejidad
425
- """
426
- if analyzed_tokens is None:
427
- analyzed_tokens = set()
428
-
429
- # Evitar ciclos
430
- if token.i in analyzed_tokens:
431
- return {
432
- 'depths': [],
433
- 'relations': {},
434
- 'complexity_score': 0
435
- }
436
-
437
- analyzed_tokens.add(token.i)
438
-
439
- # Pesos para diferentes tipos de dependencias
440
- dependency_weights = {
441
- # Dependencias principales
442
- 'nsubj': 1.2, # Sujeto nominal
443
- 'obj': 1.1, # Objeto directo
444
- 'iobj': 1.1, # Objeto indirecto
445
- 'ROOT': 1.3, # Raíz
446
-
447
- # Modificadores
448
- 'amod': 0.8, # Modificador adjetival
449
- 'advmod': 0.8, # Modificador adverbial
450
- 'nmod': 0.9, # Modificador nominal
451
-
452
- # Estructuras complejas
453
- 'csubj': 1.4, # Cláusula como sujeto
454
- 'ccomp': 1.3, # Complemento clausal
455
- 'xcomp': 1.2, # Complemento clausal abierto
456
- 'advcl': 1.2, # Cláusula adverbial
457
-
458
- # Coordinación y subordinación
459
- 'conj': 1.1, # Conjunción
460
- 'cc': 0.7, # Coordinación
461
- 'mark': 0.8, # Marcador
462
-
463
- # Otros
464
- 'det': 0.5, # Determinante
465
- 'case': 0.5, # Caso
466
- 'punct': 0.1 # Puntuación
467
- }
468
-
469
- # Inicializar resultados
470
- current_result = {
471
- 'depths': [depth],
472
- 'relations': {token.dep_: 1},
473
- 'complexity_score': dependency_weights.get(token.dep_, 0.5) * (depth + 1)
474
- }
475
-
476
- # Analizar hijos recursivamente
477
- for child in token.children:
478
- child_result = get_dependency_depths(child, depth + 1, analyzed_tokens)
479
-
480
- # Combinar profundidades
481
- current_result['depths'].extend(child_result['depths'])
482
-
483
- # Combinar relaciones
484
- for rel, count in child_result['relations'].items():
485
- current_result['relations'][rel] = current_result['relations'].get(rel, 0) + count
486
-
487
- # Acumular score de complejidad
488
- current_result['complexity_score'] += child_result['complexity_score']
489
-
490
- # Calcular métricas adicionales
491
- current_result['max_depth'] = max(current_result['depths'])
492
- current_result['avg_depth'] = sum(current_result['depths']) / len(current_result['depths'])
493
- current_result['relation_diversity'] = len(current_result['relations'])
494
-
495
- # Calcular score ponderado por tipo de estructura
496
- structure_bonus = 0
497
-
498
- # Bonus por estructuras complejas
499
- if 'csubj' in current_result['relations'] or 'ccomp' in current_result['relations']:
500
- structure_bonus += 0.3
501
-
502
- # Bonus por coordinación balanceada
503
- if 'conj' in current_result['relations'] and 'cc' in current_result['relations']:
504
- structure_bonus += 0.2
505
-
506
- # Bonus por modificación rica
507
- if len(set(['amod', 'advmod', 'nmod']) & set(current_result['relations'])) >= 2:
508
- structure_bonus += 0.2
509
-
510
- current_result['final_score'] = (
511
- current_result['complexity_score'] * (1 + structure_bonus)
512
- )
513
-
514
- return current_result
515
-
516
- def normalize_score(value, metric_type,
517
- min_threshold=0.0, target_threshold=1.0,
518
- range_factor=2.0, optimal_length=None,
519
- optimal_connections=None, optimal_depth=None):
520
- """
521
- Normaliza un valor considerando umbrales específicos por tipo de métrica.
522
-
523
- Args:
524
- value: Valor a normalizar
525
- metric_type: Tipo de métrica ('vocabulary', 'structure', 'cohesion', 'clarity')
526
- min_threshold: Valor mínimo aceptable
527
- target_threshold: Valor objetivo
528
- range_factor: Factor para ajustar el rango
529
- optimal_length: Longitud óptima (opcional)
530
- optimal_connections: Número óptimo de conexiones (opcional)
531
- optimal_depth: Profundidad óptima de estructura (opcional)
532
-
533
- Returns:
534
- float: Valor normalizado entre 0 y 1
535
- """
536
- try:
537
- # Definir umbrales por tipo de métrica
538
- METRIC_THRESHOLDS = {
539
- 'vocabulary': {
540
- 'min': 0.60,
541
- 'target': 0.75,
542
- 'range_factor': 1.5
543
- },
544
- 'structure': {
545
- 'min': 0.65,
546
- 'target': 0.80,
547
- 'range_factor': 1.8
548
- },
549
- 'cohesion': {
550
- 'min': 0.55,
551
- 'target': 0.70,
552
- 'range_factor': 1.6
553
- },
554
- 'clarity': {
555
- 'min': 0.60,
556
- 'target': 0.75,
557
- 'range_factor': 1.7
558
- }
559
- }
560
-
561
- # Validar valores negativos o cero
562
- if value < 0:
563
- logger.warning(f"Valor negativo recibido: {value}")
564
- return 0.0
565
-
566
- # Manejar caso donde el valor es cero
567
- if value == 0:
568
- logger.warning("Valor cero recibido")
569
- return 0.0
570
-
571
- # Obtener umbrales específicos para el tipo de métrica
572
- thresholds = METRIC_THRESHOLDS.get(metric_type, {
573
- 'min': min_threshold,
574
- 'target': target_threshold,
575
- 'range_factor': range_factor
576
- })
577
-
578
- # Identificar el valor de referencia a usar
579
- if optimal_depth is not None:
580
- reference = optimal_depth
581
- elif optimal_connections is not None:
582
- reference = optimal_connections
583
- elif optimal_length is not None:
584
- reference = optimal_length
585
- else:
586
- reference = thresholds['target']
587
-
588
- # Validar valor de referencia
589
- if reference <= 0:
590
- logger.warning(f"Valor de referencia inválido: {reference}")
591
- return 0.0
592
-
593
- # Calcular score basado en umbrales
594
- if value < thresholds['min']:
595
- # Valor por debajo del mínimo
596
- score = (value / thresholds['min']) * 0.5 # Máximo 0.5 para valores bajo el mínimo
597
- elif value < thresholds['target']:
598
- # Valor entre mínimo y objetivo
599
- range_size = thresholds['target'] - thresholds['min']
600
- progress = (value - thresholds['min']) / range_size
601
- score = 0.5 + (progress * 0.5) # Escala entre 0.5 y 1.0
602
- else:
603
- # Valor alcanza o supera el objetivo
604
- score = 1.0
605
-
606
- # Penalizar valores muy por encima del objetivo
607
- if value > (thresholds['target'] * thresholds['range_factor']):
608
- excess = (value - thresholds['target']) / (thresholds['target'] * thresholds['range_factor'])
609
- score = max(0.7, 1.0 - excess) # No bajar de 0.7 para valores altos
610
-
611
- # Asegurar que el resultado esté entre 0 y 1
612
- return max(0.0, min(1.0, score))
613
-
614
- except Exception as e:
615
- logger.error(f"Error en normalize_score: {str(e)}")
616
- return 0.0
617
-
618
-
619
- # Funciones de generación de gráficos
620
- def generate_sentence_graphs(doc):
621
- """Genera visualizaciones de estructura de oraciones"""
622
- fig, ax = plt.subplots(figsize=(10, 6))
623
- # Implementar visualización
624
- plt.close()
625
- return fig
626
-
627
- def generate_word_connections(doc):
628
- """Genera red de conexiones de palabras"""
629
- fig, ax = plt.subplots(figsize=(10, 6))
630
- # Implementar visualización
631
- plt.close()
632
- return fig
633
-
634
- def generate_connection_paths(doc):
635
- """Genera patrones de conexión"""
636
- fig, ax = plt.subplots(figsize=(10, 6))
637
- # Implementar visualización
638
- plt.close()
639
- return fig
640
-
641
- def create_vocabulary_network(doc):
642
- """
643
- Genera el grafo de red de vocabulario.
644
- """
645
- G = nx.Graph()
646
-
647
- # Crear nodos para palabras significativas
648
- words = [token.text.lower() for token in doc if token.is_alpha and not token.is_stop]
649
- word_freq = Counter(words)
650
-
651
- # Añadir nodos con tamaño basado en frecuencia
652
- for word, freq in word_freq.items():
653
- G.add_node(word, size=freq)
654
-
655
- # Crear conexiones basadas en co-ocurrencia
656
- window_size = 5
657
- for i in range(len(words) - window_size):
658
- window = words[i:i+window_size]
659
- for w1, w2 in combinations(set(window), 2):
660
- if G.has_edge(w1, w2):
661
- G[w1][w2]['weight'] += 1
662
- else:
663
- G.add_edge(w1, w2, weight=1)
664
-
665
- # Crear visualización
666
- fig, ax = plt.subplots(figsize=(12, 8))
667
- pos = nx.spring_layout(G)
668
-
669
- # Dibujar nodos
670
- nx.draw_networkx_nodes(G, pos,
671
- node_size=[G.nodes[node]['size']*100 for node in G.nodes],
672
- node_color='lightblue',
673
- alpha=0.7)
674
-
675
- # Dibujar conexiones
676
- nx.draw_networkx_edges(G, pos,
677
- width=[G[u][v]['weight']*0.5 for u,v in G.edges],
678
- alpha=0.5)
679
-
680
- # Añadir etiquetas
681
- nx.draw_networkx_labels(G, pos)
682
-
683
- plt.title("Red de Vocabulario")
684
- plt.axis('off')
685
- return fig
686
-
687
- def create_syntax_complexity_graph(doc):
688
- """
689
- Genera el diagrama de arco de complejidad sintáctica.
690
- Muestra la estructura de dependencias con colores basados en la complejidad.
691
- """
692
- try:
693
- # Preparar datos para la visualización
694
- sentences = list(doc.sents)
695
- if not sentences:
696
- return None
697
-
698
- # Crear figura para el gráfico
699
- fig, ax = plt.subplots(figsize=(12, len(sentences) * 2))
700
-
701
- # Colores para diferentes niveles de profundidad
702
- depth_colors = plt.cm.viridis(np.linspace(0, 1, 6))
703
-
704
- y_offset = 0
705
- max_x = 0
706
-
707
- for sent in sentences:
708
- words = [token.text for token in sent]
709
- x_positions = range(len(words))
710
- max_x = max(max_x, len(words))
711
-
712
- # Dibujar palabras
713
- plt.plot(x_positions, [y_offset] * len(words), 'k-', alpha=0.2)
714
- plt.scatter(x_positions, [y_offset] * len(words), alpha=0)
715
-
716
- # Añadir texto
717
- for i, word in enumerate(words):
718
- plt.annotate(word, (i, y_offset), xytext=(0, -10),
719
- textcoords='offset points', ha='center')
720
-
721
- # Dibujar arcos de dependencia
722
- for token in sent:
723
- if token.dep_ != "ROOT":
724
- # Calcular profundidad de dependencia
725
- depth = 0
726
- current = token
727
- while current.head != current:
728
- depth += 1
729
- current = current.head
730
-
731
- # Determinar posiciones para el arco
732
- start = token.i - sent[0].i
733
- end = token.head.i - sent[0].i
734
-
735
- # Altura del arco basada en la distancia entre palabras
736
- height = 0.5 * abs(end - start)
737
-
738
- # Color basado en la profundidad
739
- color = depth_colors[min(depth, len(depth_colors)-1)]
740
-
741
- # Crear arco
742
- arc = patches.Arc((min(start, end) + abs(end - start)/2, y_offset),
743
- width=abs(end - start),
744
- height=height,
745
- angle=0,
746
- theta1=0,
747
- theta2=180,
748
- color=color,
749
- alpha=0.6)
750
- ax.add_patch(arc)
751
-
752
- y_offset -= 2
753
-
754
- # Configurar el gráfico
755
- plt.xlim(-1, max_x)
756
- plt.ylim(y_offset - 1, 1)
757
- plt.axis('off')
758
- plt.title("Complejidad Sintáctica")
759
-
760
- return fig
761
-
762
- except Exception as e:
763
- logger.error(f"Error en create_syntax_complexity_graph: {str(e)}")
764
- return None
765
-
766
-
767
- def create_cohesion_heatmap(doc):
768
- """Genera un mapa de calor que muestra la cohesión entre párrafos/oraciones."""
769
- try:
770
- sentences = list(doc.sents)
771
- n_sentences = len(sentences)
772
-
773
- if n_sentences < 2:
774
- return None
775
-
776
- similarity_matrix = np.zeros((n_sentences, n_sentences))
777
-
778
- for i in range(n_sentences):
779
- for j in range(n_sentences):
780
- sent1_lemmas = {token.lemma_ for token in sentences[i]
781
- if token.is_alpha and not token.is_stop}
782
- sent2_lemmas = {token.lemma_ for token in sentences[j]
783
- if token.is_alpha and not token.is_stop}
784
-
785
- if sent1_lemmas and sent2_lemmas:
786
- intersection = len(sent1_lemmas & sent2_lemmas) # Corregido aquí
787
- union = len(sent1_lemmas | sent2_lemmas) # Y aquí
788
- similarity_matrix[i, j] = intersection / union if union > 0 else 0
789
-
790
- # Crear visualización
791
- fig, ax = plt.subplots(figsize=(10, 8))
792
-
793
- sns.heatmap(similarity_matrix,
794
- cmap='YlOrRd',
795
- square=True,
796
- xticklabels=False,
797
- yticklabels=False,
798
- cbar_kws={'label': 'Cohesión'},
799
- ax=ax)
800
-
801
- plt.title("Mapa de Cohesión Textual")
802
- plt.xlabel("Oraciones")
803
- plt.ylabel("Oraciones")
804
-
805
- plt.tight_layout()
806
- return fig
807
-
808
- except Exception as e:
809
- logger.error(f"Error en create_cohesion_heatmap: {str(e)}")
810
- return None
 
1
+ #v3/modules/studentact/current_situation_analysis.py
2
+
3
+ import streamlit as st
4
+ import matplotlib.pyplot as plt
5
+ import networkx as nx
6
+ import seaborn as sns
7
+ from collections import Counter
8
+ from itertools import combinations
9
+ import numpy as np
10
+ import matplotlib.patches as patches
11
+ import logging
12
+
13
+ # 2. Configuración básica del logging
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
17
+ handlers=[
18
+ logging.StreamHandler(),
19
+ logging.FileHandler('app.log')
20
+ ]
21
+ )
22
+
23
+ # 3. Obtener el logger específico para este módulo
24
+ logger = logging.getLogger(__name__)
25
+
26
+ #########################################################################
27
+
28
+ def correlate_metrics(scores):
29
+ """
30
+ Ajusta los scores para mantener correlaciones lógicas entre métricas.
31
+
32
+ Args:
33
+ scores: dict con scores iniciales de vocabulario, estructura, cohesión y claridad
34
+
35
+ Returns:
36
+ dict con scores ajustados
37
+ """
38
+ try:
39
+ # 1. Correlación estructura-cohesión
40
+ # La cohesión no puede ser menor que estructura * 0.7
41
+ min_cohesion = scores['structure']['normalized_score'] * 0.7
42
+ if scores['cohesion']['normalized_score'] < min_cohesion:
43
+ scores['cohesion']['normalized_score'] = min_cohesion
44
+
45
+ # 2. Correlación vocabulario-cohesión
46
+ # La cohesión léxica depende del vocabulario
47
+ vocab_influence = scores['vocabulary']['normalized_score'] * 0.6
48
+ scores['cohesion']['normalized_score'] = max(
49
+ scores['cohesion']['normalized_score'],
50
+ vocab_influence
51
+ )
52
+
53
+ # 3. Correlación cohesión-claridad
54
+ # La claridad no puede superar cohesión * 1.2
55
+ max_clarity = scores['cohesion']['normalized_score'] * 1.2
56
+ if scores['clarity']['normalized_score'] > max_clarity:
57
+ scores['clarity']['normalized_score'] = max_clarity
58
+
59
+ # 4. Correlación estructura-claridad
60
+ # La claridad no puede superar estructura * 1.1
61
+ struct_max_clarity = scores['structure']['normalized_score'] * 1.1
62
+ scores['clarity']['normalized_score'] = min(
63
+ scores['clarity']['normalized_score'],
64
+ struct_max_clarity
65
+ )
66
+
67
+ # Normalizar todos los scores entre 0 y 1
68
+ for metric in scores:
69
+ scores[metric]['normalized_score'] = max(0.0, min(1.0, scores[metric]['normalized_score']))
70
+
71
+ return scores
72
+
73
+ except Exception as e:
74
+ logger.error(f"Error en correlate_metrics: {str(e)}")
75
+ return scores
76
+
77
+ ##########################################################################
78
+
79
+ def analyze_text_dimensions(doc):
80
+ """
81
+ Analiza las dimensiones principales del texto manteniendo correlaciones lógicas.
82
+ """
83
+ try:
84
+ # Obtener scores iniciales
85
+ vocab_score, vocab_details = analyze_vocabulary_diversity(doc)
86
+ struct_score = analyze_structure(doc)
87
+ cohesion_score = analyze_cohesion(doc)
88
+ clarity_score, clarity_details = analyze_clarity(doc)
89
+
90
+ # Crear diccionario de scores inicial
91
+ scores = {
92
+ 'vocabulary': {
93
+ 'normalized_score': vocab_score,
94
+ 'details': vocab_details
95
+ },
96
+ 'structure': {
97
+ 'normalized_score': struct_score,
98
+ 'details': None
99
+ },
100
+ 'cohesion': {
101
+ 'normalized_score': cohesion_score,
102
+ 'details': None
103
+ },
104
+ 'clarity': {
105
+ 'normalized_score': clarity_score,
106
+ 'details': clarity_details
107
+ }
108
+ }
109
+
110
+ # Ajustar correlaciones entre métricas
111
+ adjusted_scores = correlate_metrics(scores)
112
+
113
+ # Logging para diagnóstico
114
+ logger.info(f"""
115
+ Scores originales vs ajustados:
116
+ Vocabulario: {vocab_score:.2f} -> {adjusted_scores['vocabulary']['normalized_score']:.2f}
117
+ Estructura: {struct_score:.2f} -> {adjusted_scores['structure']['normalized_score']:.2f}
118
+ Cohesión: {cohesion_score:.2f} -> {adjusted_scores['cohesion']['normalized_score']:.2f}
119
+ Claridad: {clarity_score:.2f} -> {adjusted_scores['clarity']['normalized_score']:.2f}
120
+ """)
121
+
122
+ return adjusted_scores
123
+
124
+ except Exception as e:
125
+ logger.error(f"Error en analyze_text_dimensions: {str(e)}")
126
+ return {
127
+ 'vocabulary': {'normalized_score': 0.0, 'details': {}},
128
+ 'structure': {'normalized_score': 0.0, 'details': {}},
129
+ 'cohesion': {'normalized_score': 0.0, 'details': {}},
130
+ 'clarity': {'normalized_score': 0.0, 'details': {}}
131
+ }
132
+
133
+
134
+
135
+ #############################################################################################
136
+
137
+ def analyze_clarity(doc):
138
+ """
139
+ Analiza la claridad del texto considerando múltiples factores.
140
+ """
141
+ try:
142
+ sentences = list(doc.sents)
143
+ if not sentences:
144
+ return 0.0, {}
145
+
146
+ # 1. Longitud de oraciones
147
+ sentence_lengths = [len(sent) for sent in sentences]
148
+ avg_length = sum(sentence_lengths) / len(sentences)
149
+
150
+ # Normalizar usando los umbrales definidos para clarity
151
+ length_score = normalize_score(
152
+ value=avg_length,
153
+ metric_type='clarity',
154
+ optimal_length=20, # Una oración ideal tiene ~20 palabras
155
+ min_threshold=0.60, # Consistente con METRIC_THRESHOLDS
156
+ target_threshold=0.75 # Consistente con METRIC_THRESHOLDS
157
+ )
158
+
159
+ # 2. Análisis de conectores
160
+ connector_count = 0
161
+ connector_weights = {
162
+ 'CCONJ': 1.0, # Coordinantes
163
+ 'SCONJ': 1.2, # Subordinantes
164
+ 'ADV': 0.8 # Adverbios conectivos
165
+ }
166
+
167
+ for token in doc:
168
+ if token.pos_ in connector_weights and token.dep_ in ['cc', 'mark', 'advmod']:
169
+ connector_count += connector_weights[token.pos_]
170
+
171
+ # Normalizar conectores por oración
172
+ connectors_per_sentence = connector_count / len(sentences) if sentences else 0
173
+ connector_score = normalize_score(
174
+ value=connectors_per_sentence,
175
+ metric_type='clarity',
176
+ optimal_connections=1.5, # ~1.5 conectores por oración es óptimo
177
+ min_threshold=0.60,
178
+ target_threshold=0.75
179
+ )
180
+
181
+ # 3. Complejidad estructural
182
+ clause_count = 0
183
+ for sent in sentences:
184
+ verbs = [token for token in sent if token.pos_ == 'VERB']
185
+ clause_count += len(verbs)
186
+
187
+ complexity_raw = clause_count / len(sentences) if sentences else 0
188
+ complexity_score = normalize_score(
189
+ value=complexity_raw,
190
+ metric_type='clarity',
191
+ optimal_depth=2.0, # ~2 cláusulas por oración es óptimo
192
+ min_threshold=0.60,
193
+ target_threshold=0.75
194
+ )
195
+
196
+ # 4. Densidad léxica
197
+ content_words = len([token for token in doc if token.pos_ in ['NOUN', 'VERB', 'ADJ', 'ADV']])
198
+ total_words = len([token for token in doc if token.is_alpha])
199
+ density = content_words / total_words if total_words > 0 else 0
200
+
201
+ density_score = normalize_score(
202
+ value=density,
203
+ metric_type='clarity',
204
+ optimal_connections=0.6, # 60% de palabras de contenido es óptimo
205
+ min_threshold=0.60,
206
+ target_threshold=0.75
207
+ )
208
+
209
+ # Score final ponderado
210
+ weights = {
211
+ 'length': 0.3,
212
+ 'connectors': 0.3,
213
+ 'complexity': 0.2,
214
+ 'density': 0.2
215
+ }
216
+
217
+ clarity_score = (
218
+ weights['length'] * length_score +
219
+ weights['connectors'] * connector_score +
220
+ weights['complexity'] * complexity_score +
221
+ weights['density'] * density_score
222
+ )
223
+
224
+ details = {
225
+ 'length_score': length_score,
226
+ 'connector_score': connector_score,
227
+ 'complexity_score': complexity_score,
228
+ 'density_score': density_score,
229
+ 'avg_sentence_length': avg_length,
230
+ 'connectors_per_sentence': connectors_per_sentence,
231
+ 'density': density
232
+ }
233
+
234
+ # Agregar logging para diagnóstico
235
+ logger.info(f"""
236
+ Scores de Claridad:
237
+ - Longitud: {length_score:.2f} (avg={avg_length:.1f} palabras)
238
+ - Conectores: {connector_score:.2f} (avg={connectors_per_sentence:.1f} por oración)
239
+ - Complejidad: {complexity_score:.2f} (avg={complexity_raw:.1f} cláusulas)
240
+ - Densidad: {density_score:.2f} ({density*100:.1f}% palabras de contenido)
241
+ - Score Final: {clarity_score:.2f}
242
+ """)
243
+
244
+ return clarity_score, details
245
+
246
+ except Exception as e:
247
+ logger.error(f"Error en analyze_clarity: {str(e)}")
248
+ return 0.0, {}
249
+
250
+
251
+ def analyze_vocabulary_diversity(doc):
252
+ """Análisis mejorado de la diversidad y calidad del vocabulario"""
253
+ try:
254
+ # 1. Análisis básico de diversidad
255
+ unique_lemmas = {token.lemma_ for token in doc if token.is_alpha}
256
+ total_words = len([token for token in doc if token.is_alpha])
257
+ basic_diversity = len(unique_lemmas) / total_words if total_words > 0 else 0
258
+
259
+ # 2. Análisis de registro
260
+ academic_words = 0
261
+ narrative_words = 0
262
+ technical_terms = 0
263
+
264
+ # Clasificar palabras por registro
265
+ for token in doc:
266
+ if token.is_alpha:
267
+ # Detectar términos académicos/técnicos
268
+ if token.pos_ in ['NOUN', 'VERB', 'ADJ']:
269
+ if any(parent.pos_ == 'NOUN' for parent in token.ancestors):
270
+ technical_terms += 1
271
+ # Detectar palabras narrativas
272
+ if token.pos_ in ['VERB', 'ADV'] and token.dep_ in ['ROOT', 'advcl']:
273
+ narrative_words += 1
274
+
275
+ # 3. Análisis de complejidad sintáctica
276
+ avg_sentence_length = sum(len(sent) for sent in doc.sents) / len(list(doc.sents))
277
+
278
+ # 4. Calcular score ponderado
279
+ weights = {
280
+ 'diversity': 0.3,
281
+ 'technical': 0.3,
282
+ 'narrative': 0.2,
283
+ 'complexity': 0.2
284
+ }
285
+
286
+ scores = {
287
+ 'diversity': basic_diversity,
288
+ 'technical': technical_terms / total_words if total_words > 0 else 0,
289
+ 'narrative': narrative_words / total_words if total_words > 0 else 0,
290
+ 'complexity': min(1.0, avg_sentence_length / 20) # Normalizado a 20 palabras
291
+ }
292
+
293
+ # Score final ponderado
294
+ final_score = sum(weights[key] * scores[key] for key in weights)
295
+
296
+ # Información adicional para diagnóstico
297
+ details = {
298
+ 'text_type': 'narrative' if scores['narrative'] > scores['technical'] else 'academic',
299
+ 'scores': scores
300
+ }
301
+
302
+ return final_score, details
303
+
304
+ except Exception as e:
305
+ logger.error(f"Error en analyze_vocabulary_diversity: {str(e)}")
306
+ return 0.0, {}
307
+
308
+ def analyze_cohesion(doc):
309
+ """Analiza la cohesión textual"""
310
+ try:
311
+ sentences = list(doc.sents)
312
+ if len(sentences) < 2:
313
+ logger.warning("Texto demasiado corto para análisis de cohesión")
314
+ return 0.0
315
+
316
+ # 1. Análisis de conexiones léxicas
317
+ lexical_connections = 0
318
+ total_possible_connections = 0
319
+
320
+ for i in range(len(sentences)-1):
321
+ # Obtener lemmas significativos (no stopwords)
322
+ sent1_words = {token.lemma_ for token in sentences[i]
323
+ if token.is_alpha and not token.is_stop}
324
+ sent2_words = {token.lemma_ for token in sentences[i+1]
325
+ if token.is_alpha and not token.is_stop}
326
+
327
+ if sent1_words and sent2_words: # Verificar que ambos conjuntos no estén vacíos
328
+ intersection = len(sent1_words.intersection(sent2_words))
329
+ total_possible = min(len(sent1_words), len(sent2_words))
330
+
331
+ if total_possible > 0:
332
+ lexical_score = intersection / total_possible
333
+ lexical_connections += lexical_score
334
+ total_possible_connections += 1
335
+
336
+ # 2. Análisis de conectores
337
+ connector_count = 0
338
+ connector_types = {
339
+ 'CCONJ': 1.0, # Coordinantes
340
+ 'SCONJ': 1.2, # Subordinantes
341
+ 'ADV': 0.8 # Adverbios conectivos
342
+ }
343
+
344
+ for token in doc:
345
+ if (token.pos_ in connector_types and
346
+ token.dep_ in ['cc', 'mark', 'advmod'] and
347
+ not token.is_stop):
348
+ connector_count += connector_types[token.pos_]
349
+
350
+ # 3. Cálculo de scores normalizados
351
+ if total_possible_connections > 0:
352
+ lexical_cohesion = lexical_connections / total_possible_connections
353
+ else:
354
+ lexical_cohesion = 0
355
+
356
+ if len(sentences) > 1:
357
+ connector_cohesion = min(1.0, connector_count / (len(sentences) - 1))
358
+ else:
359
+ connector_cohesion = 0
360
+
361
+ # 4. Score final ponderado
362
+ weights = {
363
+ 'lexical': 0.7,
364
+ 'connectors': 0.3
365
+ }
366
+
367
+ cohesion_score = (
368
+ weights['lexical'] * lexical_cohesion +
369
+ weights['connectors'] * connector_cohesion
370
+ )
371
+
372
+ # 5. Logging para diagnóstico
373
+ logger.info(f"""
374
+ Análisis de Cohesión:
375
+ - Conexiones léxicas encontradas: {lexical_connections}
376
+ - Conexiones posibles: {total_possible_connections}
377
+ - Lexical cohesion score: {lexical_cohesion}
378
+ - Conectores encontrados: {connector_count}
379
+ - Connector cohesion score: {connector_cohesion}
380
+ - Score final: {cohesion_score}
381
+ """)
382
+
383
+ return cohesion_score
384
+
385
+ except Exception as e:
386
+ logger.error(f"Error en analyze_cohesion: {str(e)}")
387
+ return 0.0
388
+
389
+ def analyze_structure(doc):
390
+ try:
391
+ if len(doc) == 0:
392
+ return 0.0
393
+
394
+ structure_scores = []
395
+ for token in doc:
396
+ if token.dep_ == 'ROOT':
397
+ result = get_dependency_depths(token)
398
+ structure_scores.append(result['final_score'])
399
+
400
+ if not structure_scores:
401
+ return 0.0
402
+
403
+ return min(1.0, sum(structure_scores) / len(structure_scores))
404
+
405
+ except Exception as e:
406
+ logger.error(f"Error en analyze_structure: {str(e)}")
407
+ return 0.0
408
+
409
+ # Funciones auxiliares de análisis
410
+
411
+ def get_dependency_depths(token, depth=0, analyzed_tokens=None):
412
+ """
413
+ Analiza la profundidad y calidad de las relaciones de dependencia.
414
+
415
+ Args:
416
+ token: Token a analizar
417
+ depth: Profundidad actual en el árbol
418
+ analyzed_tokens: Set para evitar ciclos en el análisis
419
+
420
+ Returns:
421
+ dict: Información detallada sobre las dependencias
422
+ - depths: Lista de profundidades
423
+ - relations: Diccionario con tipos de relaciones encontradas
424
+ - complexity_score: Puntuación de complejidad
425
+ """
426
+ if analyzed_tokens is None:
427
+ analyzed_tokens = set()
428
+
429
+ # Evitar ciclos
430
+ if token.i in analyzed_tokens:
431
+ return {
432
+ 'depths': [],
433
+ 'relations': {},
434
+ 'complexity_score': 0
435
+ }
436
+
437
+ analyzed_tokens.add(token.i)
438
+
439
+ # Pesos para diferentes tipos de dependencias
440
+ dependency_weights = {
441
+ # Dependencias principales
442
+ 'nsubj': 1.2, # Sujeto nominal
443
+ 'obj': 1.1, # Objeto directo
444
+ 'iobj': 1.1, # Objeto indirecto
445
+ 'ROOT': 1.3, # Raíz
446
+
447
+ # Modificadores
448
+ 'amod': 0.8, # Modificador adjetival
449
+ 'advmod': 0.8, # Modificador adverbial
450
+ 'nmod': 0.9, # Modificador nominal
451
+
452
+ # Estructuras complejas
453
+ 'csubj': 1.4, # Cláusula como sujeto
454
+ 'ccomp': 1.3, # Complemento clausal
455
+ 'xcomp': 1.2, # Complemento clausal abierto
456
+ 'advcl': 1.2, # Cláusula adverbial
457
+
458
+ # Coordinación y subordinación
459
+ 'conj': 1.1, # Conjunción
460
+ 'cc': 0.7, # Coordinación
461
+ 'mark': 0.8, # Marcador
462
+
463
+ # Otros
464
+ 'det': 0.5, # Determinante
465
+ 'case': 0.5, # Caso
466
+ 'punct': 0.1 # Puntuación
467
+ }
468
+
469
+ # Inicializar resultados
470
+ current_result = {
471
+ 'depths': [depth],
472
+ 'relations': {token.dep_: 1},
473
+ 'complexity_score': dependency_weights.get(token.dep_, 0.5) * (depth + 1)
474
+ }
475
+
476
+ # Analizar hijos recursivamente
477
+ for child in token.children:
478
+ child_result = get_dependency_depths(child, depth + 1, analyzed_tokens)
479
+
480
+ # Combinar profundidades
481
+ current_result['depths'].extend(child_result['depths'])
482
+
483
+ # Combinar relaciones
484
+ for rel, count in child_result['relations'].items():
485
+ current_result['relations'][rel] = current_result['relations'].get(rel, 0) + count
486
+
487
+ # Acumular score de complejidad
488
+ current_result['complexity_score'] += child_result['complexity_score']
489
+
490
+ # Calcular métricas adicionales
491
+ current_result['max_depth'] = max(current_result['depths'])
492
+ current_result['avg_depth'] = sum(current_result['depths']) / len(current_result['depths'])
493
+ current_result['relation_diversity'] = len(current_result['relations'])
494
+
495
+ # Calcular score ponderado por tipo de estructura
496
+ structure_bonus = 0
497
+
498
+ # Bonus por estructuras complejas
499
+ if 'csubj' in current_result['relations'] or 'ccomp' in current_result['relations']:
500
+ structure_bonus += 0.3
501
+
502
+ # Bonus por coordinación balanceada
503
+ if 'conj' in current_result['relations'] and 'cc' in current_result['relations']:
504
+ structure_bonus += 0.2
505
+
506
+ # Bonus por modificación rica
507
+ if len(set(['amod', 'advmod', 'nmod']) & set(current_result['relations'])) >= 2:
508
+ structure_bonus += 0.2
509
+
510
+ current_result['final_score'] = (
511
+ current_result['complexity_score'] * (1 + structure_bonus)
512
+ )
513
+
514
+ return current_result
515
+
516
+ def normalize_score(value, metric_type,
517
+ min_threshold=0.0, target_threshold=1.0,
518
+ range_factor=2.0, optimal_length=None,
519
+ optimal_connections=None, optimal_depth=None):
520
+ """
521
+ Normaliza un valor considerando umbrales específicos por tipo de métrica.
522
+
523
+ Args:
524
+ value: Valor a normalizar
525
+ metric_type: Tipo de métrica ('vocabulary', 'structure', 'cohesion', 'clarity')
526
+ min_threshold: Valor mínimo aceptable
527
+ target_threshold: Valor objetivo
528
+ range_factor: Factor para ajustar el rango
529
+ optimal_length: Longitud óptima (opcional)
530
+ optimal_connections: Número óptimo de conexiones (opcional)
531
+ optimal_depth: Profundidad óptima de estructura (opcional)
532
+
533
+ Returns:
534
+ float: Valor normalizado entre 0 y 1
535
+ """
536
+ try:
537
+ # Definir umbrales por tipo de métrica
538
+ METRIC_THRESHOLDS = {
539
+ 'vocabulary': {
540
+ 'min': 0.60,
541
+ 'target': 0.75,
542
+ 'range_factor': 1.5
543
+ },
544
+ 'structure': {
545
+ 'min': 0.65,
546
+ 'target': 0.80,
547
+ 'range_factor': 1.8
548
+ },
549
+ 'cohesion': {
550
+ 'min': 0.55,
551
+ 'target': 0.70,
552
+ 'range_factor': 1.6
553
+ },
554
+ 'clarity': {
555
+ 'min': 0.60,
556
+ 'target': 0.75,
557
+ 'range_factor': 1.7
558
+ }
559
+ }
560
+
561
+ # Validar valores negativos o cero
562
+ if value < 0:
563
+ logger.warning(f"Valor negativo recibido: {value}")
564
+ return 0.0
565
+
566
+ # Manejar caso donde el valor es cero
567
+ if value == 0:
568
+ logger.warning("Valor cero recibido")
569
+ return 0.0
570
+
571
+ # Obtener umbrales específicos para el tipo de métrica
572
+ thresholds = METRIC_THRESHOLDS.get(metric_type, {
573
+ 'min': min_threshold,
574
+ 'target': target_threshold,
575
+ 'range_factor': range_factor
576
+ })
577
+
578
+ # Identificar el valor de referencia a usar
579
+ if optimal_depth is not None:
580
+ reference = optimal_depth
581
+ elif optimal_connections is not None:
582
+ reference = optimal_connections
583
+ elif optimal_length is not None:
584
+ reference = optimal_length
585
+ else:
586
+ reference = thresholds['target']
587
+
588
+ # Validar valor de referencia
589
+ if reference <= 0:
590
+ logger.warning(f"Valor de referencia inválido: {reference}")
591
+ return 0.0
592
+
593
+ # Calcular score basado en umbrales
594
+ if value < thresholds['min']:
595
+ # Valor por debajo del mínimo
596
+ score = (value / thresholds['min']) * 0.5 # Máximo 0.5 para valores bajo el mínimo
597
+ elif value < thresholds['target']:
598
+ # Valor entre mínimo y objetivo
599
+ range_size = thresholds['target'] - thresholds['min']
600
+ progress = (value - thresholds['min']) / range_size
601
+ score = 0.5 + (progress * 0.5) # Escala entre 0.5 y 1.0
602
+ else:
603
+ # Valor alcanza o supera el objetivo
604
+ score = 1.0
605
+
606
+ # Penalizar valores muy por encima del objetivo
607
+ if value > (thresholds['target'] * thresholds['range_factor']):
608
+ excess = (value - thresholds['target']) / (thresholds['target'] * thresholds['range_factor'])
609
+ score = max(0.7, 1.0 - excess) # No bajar de 0.7 para valores altos
610
+
611
+ # Asegurar que el resultado esté entre 0 y 1
612
+ return max(0.0, min(1.0, score))
613
+
614
+ except Exception as e:
615
+ logger.error(f"Error en normalize_score: {str(e)}")
616
+ return 0.0
617
+
618
+
619
+ # Funciones de generación de gráficos
620
+ def generate_sentence_graphs(doc):
621
+ """Genera visualizaciones de estructura de oraciones"""
622
+ fig, ax = plt.subplots(figsize=(10, 6))
623
+ # Implementar visualización
624
+ plt.close()
625
+ return fig
626
+
627
+ def generate_word_connections(doc):
628
+ """Genera red de conexiones de palabras"""
629
+ fig, ax = plt.subplots(figsize=(10, 6))
630
+ # Implementar visualización
631
+ plt.close()
632
+ return fig
633
+
634
+ def generate_connection_paths(doc):
635
+ """Genera patrones de conexión"""
636
+ fig, ax = plt.subplots(figsize=(10, 6))
637
+ # Implementar visualización
638
+ plt.close()
639
+ return fig
640
+
641
+ def create_vocabulary_network(doc):
642
+ """
643
+ Genera el grafo de red de vocabulario.
644
+ """
645
+ G = nx.Graph()
646
+
647
+ # Crear nodos para palabras significativas
648
+ words = [token.text.lower() for token in doc if token.is_alpha and not token.is_stop]
649
+ word_freq = Counter(words)
650
+
651
+ # Añadir nodos con tamaño basado en frecuencia
652
+ for word, freq in word_freq.items():
653
+ G.add_node(word, size=freq)
654
+
655
+ # Crear conexiones basadas en co-ocurrencia
656
+ window_size = 5
657
+ for i in range(len(words) - window_size):
658
+ window = words[i:i+window_size]
659
+ for w1, w2 in combinations(set(window), 2):
660
+ if G.has_edge(w1, w2):
661
+ G[w1][w2]['weight'] += 1
662
+ else:
663
+ G.add_edge(w1, w2, weight=1)
664
+
665
+ # Crear visualización
666
+ fig, ax = plt.subplots(figsize=(12, 8))
667
+ pos = nx.spring_layout(G)
668
+
669
+ # Dibujar nodos
670
+ nx.draw_networkx_nodes(G, pos,
671
+ node_size=[G.nodes[node]['size']*100 for node in G.nodes],
672
+ node_color='lightblue',
673
+ alpha=0.7)
674
+
675
+ # Dibujar conexiones
676
+ nx.draw_networkx_edges(G, pos,
677
+ width=[G[u][v]['weight']*0.5 for u,v in G.edges],
678
+ alpha=0.5)
679
+
680
+ # Añadir etiquetas
681
+ nx.draw_networkx_labels(G, pos)
682
+
683
+ plt.title("Red de Vocabulario")
684
+ plt.axis('off')
685
+ return fig
686
+
687
+ def create_syntax_complexity_graph(doc):
688
+ """
689
+ Genera el diagrama de arco de complejidad sintáctica.
690
+ Muestra la estructura de dependencias con colores basados en la complejidad.
691
+ """
692
+ try:
693
+ # Preparar datos para la visualización
694
+ sentences = list(doc.sents)
695
+ if not sentences:
696
+ return None
697
+
698
+ # Crear figura para el gráfico
699
+ fig, ax = plt.subplots(figsize=(12, len(sentences) * 2))
700
+
701
+ # Colores para diferentes niveles de profundidad
702
+ depth_colors = plt.cm.viridis(np.linspace(0, 1, 6))
703
+
704
+ y_offset = 0
705
+ max_x = 0
706
+
707
+ for sent in sentences:
708
+ words = [token.text for token in sent]
709
+ x_positions = range(len(words))
710
+ max_x = max(max_x, len(words))
711
+
712
+ # Dibujar palabras
713
+ plt.plot(x_positions, [y_offset] * len(words), 'k-', alpha=0.2)
714
+ plt.scatter(x_positions, [y_offset] * len(words), alpha=0)
715
+
716
+ # Añadir texto
717
+ for i, word in enumerate(words):
718
+ plt.annotate(word, (i, y_offset), xytext=(0, -10),
719
+ textcoords='offset points', ha='center')
720
+
721
+ # Dibujar arcos de dependencia
722
+ for token in sent:
723
+ if token.dep_ != "ROOT":
724
+ # Calcular profundidad de dependencia
725
+ depth = 0
726
+ current = token
727
+ while current.head != current:
728
+ depth += 1
729
+ current = current.head
730
+
731
+ # Determinar posiciones para el arco
732
+ start = token.i - sent[0].i
733
+ end = token.head.i - sent[0].i
734
+
735
+ # Altura del arco basada en la distancia entre palabras
736
+ height = 0.5 * abs(end - start)
737
+
738
+ # Color basado en la profundidad
739
+ color = depth_colors[min(depth, len(depth_colors)-1)]
740
+
741
+ # Crear arco
742
+ arc = patches.Arc((min(start, end) + abs(end - start)/2, y_offset),
743
+ width=abs(end - start),
744
+ height=height,
745
+ angle=0,
746
+ theta1=0,
747
+ theta2=180,
748
+ color=color,
749
+ alpha=0.6)
750
+ ax.add_patch(arc)
751
+
752
+ y_offset -= 2
753
+
754
+ # Configurar el gráfico
755
+ plt.xlim(-1, max_x)
756
+ plt.ylim(y_offset - 1, 1)
757
+ plt.axis('off')
758
+ plt.title("Complejidad Sintáctica")
759
+
760
+ return fig
761
+
762
+ except Exception as e:
763
+ logger.error(f"Error en create_syntax_complexity_graph: {str(e)}")
764
+ return None
765
+
766
+
767
+ def create_cohesion_heatmap(doc):
768
+ """Genera un mapa de calor que muestra la cohesión entre párrafos/oraciones."""
769
+ try:
770
+ sentences = list(doc.sents)
771
+ n_sentences = len(sentences)
772
+
773
+ if n_sentences < 2:
774
+ return None
775
+
776
+ similarity_matrix = np.zeros((n_sentences, n_sentences))
777
+
778
+ for i in range(n_sentences):
779
+ for j in range(n_sentences):
780
+ sent1_lemmas = {token.lemma_ for token in sentences[i]
781
+ if token.is_alpha and not token.is_stop}
782
+ sent2_lemmas = {token.lemma_ for token in sentences[j]
783
+ if token.is_alpha and not token.is_stop}
784
+
785
+ if sent1_lemmas and sent2_lemmas:
786
+ intersection = len(sent1_lemmas & sent2_lemmas) # Corregido aquí
787
+ union = len(sent1_lemmas | sent2_lemmas) # Y aquí
788
+ similarity_matrix[i, j] = intersection / union if union > 0 else 0
789
+
790
+ # Crear visualización
791
+ fig, ax = plt.subplots(figsize=(10, 8))
792
+
793
+ sns.heatmap(similarity_matrix,
794
+ cmap='YlOrRd',
795
+ square=True,
796
+ xticklabels=False,
797
+ yticklabels=False,
798
+ cbar_kws={'label': 'Cohesión'},
799
+ ax=ax)
800
+
801
+ plt.title("Mapa de Cohesión Textual")
802
+ plt.xlabel("Oraciones")
803
+ plt.ylabel("Oraciones")
804
+
805
+ plt.tight_layout()
806
+ return fig
807
+
808
+ except Exception as e:
809
+ logger.error(f"Error en create_cohesion_heatmap: {str(e)}")
810
+ return None
modules/studentact/current_situation_analysis.py CHANGED
@@ -1,810 +1,1008 @@
1
- #v3/modules/studentact/current_situation_analysis.py
2
-
3
- import streamlit as st
4
- import matplotlib.pyplot as plt
5
- import networkx as nx
6
- import seaborn as sns
7
- from collections import Counter
8
- from itertools import combinations
9
- import numpy as np
10
- import matplotlib.patches as patches
11
- import logging
12
-
13
- # 2. Configuración básica del logging
14
- logging.basicConfig(
15
- level=logging.INFO,
16
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
17
- handlers=[
18
- logging.StreamHandler(),
19
- logging.FileHandler('app.log')
20
- ]
21
- )
22
-
23
- # 3. Obtener el logger específico para este módulo
24
- logger = logging.getLogger(__name__)
25
-
26
- #########################################################################
27
-
28
- def correlate_metrics(scores):
29
- """
30
- Ajusta los scores para mantener correlaciones lógicas entre métricas.
31
-
32
- Args:
33
- scores: dict con scores iniciales de vocabulario, estructura, cohesión y claridad
34
-
35
- Returns:
36
- dict con scores ajustados
37
- """
38
- try:
39
- # 1. Correlación estructura-cohesión
40
- # La cohesión no puede ser menor que estructura * 0.7
41
- min_cohesion = scores['structure']['normalized_score'] * 0.7
42
- if scores['cohesion']['normalized_score'] < min_cohesion:
43
- scores['cohesion']['normalized_score'] = min_cohesion
44
-
45
- # 2. Correlación vocabulario-cohesión
46
- # La cohesión léxica depende del vocabulario
47
- vocab_influence = scores['vocabulary']['normalized_score'] * 0.6
48
- scores['cohesion']['normalized_score'] = max(
49
- scores['cohesion']['normalized_score'],
50
- vocab_influence
51
- )
52
-
53
- # 3. Correlación cohesión-claridad
54
- # La claridad no puede superar cohesión * 1.2
55
- max_clarity = scores['cohesion']['normalized_score'] * 1.2
56
- if scores['clarity']['normalized_score'] > max_clarity:
57
- scores['clarity']['normalized_score'] = max_clarity
58
-
59
- # 4. Correlación estructura-claridad
60
- # La claridad no puede superar estructura * 1.1
61
- struct_max_clarity = scores['structure']['normalized_score'] * 1.1
62
- scores['clarity']['normalized_score'] = min(
63
- scores['clarity']['normalized_score'],
64
- struct_max_clarity
65
- )
66
-
67
- # Normalizar todos los scores entre 0 y 1
68
- for metric in scores:
69
- scores[metric]['normalized_score'] = max(0.0, min(1.0, scores[metric]['normalized_score']))
70
-
71
- return scores
72
-
73
- except Exception as e:
74
- logger.error(f"Error en correlate_metrics: {str(e)}")
75
- return scores
76
-
77
- ##########################################################################
78
-
79
- def analyze_text_dimensions(doc):
80
- """
81
- Analiza las dimensiones principales del texto manteniendo correlaciones lógicas.
82
- """
83
- try:
84
- # Obtener scores iniciales
85
- vocab_score, vocab_details = analyze_vocabulary_diversity(doc)
86
- struct_score = analyze_structure(doc)
87
- cohesion_score = analyze_cohesion(doc)
88
- clarity_score, clarity_details = analyze_clarity(doc)
89
-
90
- # Crear diccionario de scores inicial
91
- scores = {
92
- 'vocabulary': {
93
- 'normalized_score': vocab_score,
94
- 'details': vocab_details
95
- },
96
- 'structure': {
97
- 'normalized_score': struct_score,
98
- 'details': None
99
- },
100
- 'cohesion': {
101
- 'normalized_score': cohesion_score,
102
- 'details': None
103
- },
104
- 'clarity': {
105
- 'normalized_score': clarity_score,
106
- 'details': clarity_details
107
- }
108
- }
109
-
110
- # Ajustar correlaciones entre métricas
111
- adjusted_scores = correlate_metrics(scores)
112
-
113
- # Logging para diagnóstico
114
- logger.info(f"""
115
- Scores originales vs ajustados:
116
- Vocabulario: {vocab_score:.2f} -> {adjusted_scores['vocabulary']['normalized_score']:.2f}
117
- Estructura: {struct_score:.2f} -> {adjusted_scores['structure']['normalized_score']:.2f}
118
- Cohesión: {cohesion_score:.2f} -> {adjusted_scores['cohesion']['normalized_score']:.2f}
119
- Claridad: {clarity_score:.2f} -> {adjusted_scores['clarity']['normalized_score']:.2f}
120
- """)
121
-
122
- return adjusted_scores
123
-
124
- except Exception as e:
125
- logger.error(f"Error en analyze_text_dimensions: {str(e)}")
126
- return {
127
- 'vocabulary': {'normalized_score': 0.0, 'details': {}},
128
- 'structure': {'normalized_score': 0.0, 'details': {}},
129
- 'cohesion': {'normalized_score': 0.0, 'details': {}},
130
- 'clarity': {'normalized_score': 0.0, 'details': {}}
131
- }
132
-
133
-
134
-
135
- #############################################################################################
136
-
137
- def analyze_clarity(doc):
138
- """
139
- Analiza la claridad del texto considerando múltiples factores.
140
- """
141
- try:
142
- sentences = list(doc.sents)
143
- if not sentences:
144
- return 0.0, {}
145
-
146
- # 1. Longitud de oraciones
147
- sentence_lengths = [len(sent) for sent in sentences]
148
- avg_length = sum(sentence_lengths) / len(sentences)
149
-
150
- # Normalizar usando los umbrales definidos para clarity
151
- length_score = normalize_score(
152
- value=avg_length,
153
- metric_type='clarity',
154
- optimal_length=20, # Una oración ideal tiene ~20 palabras
155
- min_threshold=0.60, # Consistente con METRIC_THRESHOLDS
156
- target_threshold=0.75 # Consistente con METRIC_THRESHOLDS
157
- )
158
-
159
- # 2. Análisis de conectores
160
- connector_count = 0
161
- connector_weights = {
162
- 'CCONJ': 1.0, # Coordinantes
163
- 'SCONJ': 1.2, # Subordinantes
164
- 'ADV': 0.8 # Adverbios conectivos
165
- }
166
-
167
- for token in doc:
168
- if token.pos_ in connector_weights and token.dep_ in ['cc', 'mark', 'advmod']:
169
- connector_count += connector_weights[token.pos_]
170
-
171
- # Normalizar conectores por oración
172
- connectors_per_sentence = connector_count / len(sentences) if sentences else 0
173
- connector_score = normalize_score(
174
- value=connectors_per_sentence,
175
- metric_type='clarity',
176
- optimal_connections=1.5, # ~1.5 conectores por oración es óptimo
177
- min_threshold=0.60,
178
- target_threshold=0.75
179
- )
180
-
181
- # 3. Complejidad estructural
182
- clause_count = 0
183
- for sent in sentences:
184
- verbs = [token for token in sent if token.pos_ == 'VERB']
185
- clause_count += len(verbs)
186
-
187
- complexity_raw = clause_count / len(sentences) if sentences else 0
188
- complexity_score = normalize_score(
189
- value=complexity_raw,
190
- metric_type='clarity',
191
- optimal_depth=2.0, # ~2 cláusulas por oración es óptimo
192
- min_threshold=0.60,
193
- target_threshold=0.75
194
- )
195
-
196
- # 4. Densidad léxica
197
- content_words = len([token for token in doc if token.pos_ in ['NOUN', 'VERB', 'ADJ', 'ADV']])
198
- total_words = len([token for token in doc if token.is_alpha])
199
- density = content_words / total_words if total_words > 0 else 0
200
-
201
- density_score = normalize_score(
202
- value=density,
203
- metric_type='clarity',
204
- optimal_connections=0.6, # 60% de palabras de contenido es óptimo
205
- min_threshold=0.60,
206
- target_threshold=0.75
207
- )
208
-
209
- # Score final ponderado
210
- weights = {
211
- 'length': 0.3,
212
- 'connectors': 0.3,
213
- 'complexity': 0.2,
214
- 'density': 0.2
215
- }
216
-
217
- clarity_score = (
218
- weights['length'] * length_score +
219
- weights['connectors'] * connector_score +
220
- weights['complexity'] * complexity_score +
221
- weights['density'] * density_score
222
- )
223
-
224
- details = {
225
- 'length_score': length_score,
226
- 'connector_score': connector_score,
227
- 'complexity_score': complexity_score,
228
- 'density_score': density_score,
229
- 'avg_sentence_length': avg_length,
230
- 'connectors_per_sentence': connectors_per_sentence,
231
- 'density': density
232
- }
233
-
234
- # Agregar logging para diagnóstico
235
- logger.info(f"""
236
- Scores de Claridad:
237
- - Longitud: {length_score:.2f} (avg={avg_length:.1f} palabras)
238
- - Conectores: {connector_score:.2f} (avg={connectors_per_sentence:.1f} por oración)
239
- - Complejidad: {complexity_score:.2f} (avg={complexity_raw:.1f} cláusulas)
240
- - Densidad: {density_score:.2f} ({density*100:.1f}% palabras de contenido)
241
- - Score Final: {clarity_score:.2f}
242
- """)
243
-
244
- return clarity_score, details
245
-
246
- except Exception as e:
247
- logger.error(f"Error en analyze_clarity: {str(e)}")
248
- return 0.0, {}
249
-
250
-
251
- def analyze_vocabulary_diversity(doc):
252
- """Análisis mejorado de la diversidad y calidad del vocabulario"""
253
- try:
254
- # 1. Análisis básico de diversidad
255
- unique_lemmas = {token.lemma_ for token in doc if token.is_alpha}
256
- total_words = len([token for token in doc if token.is_alpha])
257
- basic_diversity = len(unique_lemmas) / total_words if total_words > 0 else 0
258
-
259
- # 2. Análisis de registro
260
- academic_words = 0
261
- narrative_words = 0
262
- technical_terms = 0
263
-
264
- # Clasificar palabras por registro
265
- for token in doc:
266
- if token.is_alpha:
267
- # Detectar términos académicos/técnicos
268
- if token.pos_ in ['NOUN', 'VERB', 'ADJ']:
269
- if any(parent.pos_ == 'NOUN' for parent in token.ancestors):
270
- technical_terms += 1
271
- # Detectar palabras narrativas
272
- if token.pos_ in ['VERB', 'ADV'] and token.dep_ in ['ROOT', 'advcl']:
273
- narrative_words += 1
274
-
275
- # 3. Análisis de complejidad sintáctica
276
- avg_sentence_length = sum(len(sent) for sent in doc.sents) / len(list(doc.sents))
277
-
278
- # 4. Calcular score ponderado
279
- weights = {
280
- 'diversity': 0.3,
281
- 'technical': 0.3,
282
- 'narrative': 0.2,
283
- 'complexity': 0.2
284
- }
285
-
286
- scores = {
287
- 'diversity': basic_diversity,
288
- 'technical': technical_terms / total_words if total_words > 0 else 0,
289
- 'narrative': narrative_words / total_words if total_words > 0 else 0,
290
- 'complexity': min(1.0, avg_sentence_length / 20) # Normalizado a 20 palabras
291
- }
292
-
293
- # Score final ponderado
294
- final_score = sum(weights[key] * scores[key] for key in weights)
295
-
296
- # Información adicional para diagnóstico
297
- details = {
298
- 'text_type': 'narrative' if scores['narrative'] > scores['technical'] else 'academic',
299
- 'scores': scores
300
- }
301
-
302
- return final_score, details
303
-
304
- except Exception as e:
305
- logger.error(f"Error en analyze_vocabulary_diversity: {str(e)}")
306
- return 0.0, {}
307
-
308
- def analyze_cohesion(doc):
309
- """Analiza la cohesión textual"""
310
- try:
311
- sentences = list(doc.sents)
312
- if len(sentences) < 2:
313
- logger.warning("Texto demasiado corto para análisis de cohesión")
314
- return 0.0
315
-
316
- # 1. Análisis de conexiones léxicas
317
- lexical_connections = 0
318
- total_possible_connections = 0
319
-
320
- for i in range(len(sentences)-1):
321
- # Obtener lemmas significativos (no stopwords)
322
- sent1_words = {token.lemma_ for token in sentences[i]
323
- if token.is_alpha and not token.is_stop}
324
- sent2_words = {token.lemma_ for token in sentences[i+1]
325
- if token.is_alpha and not token.is_stop}
326
-
327
- if sent1_words and sent2_words: # Verificar que ambos conjuntos no estén vacíos
328
- intersection = len(sent1_words.intersection(sent2_words))
329
- total_possible = min(len(sent1_words), len(sent2_words))
330
-
331
- if total_possible > 0:
332
- lexical_score = intersection / total_possible
333
- lexical_connections += lexical_score
334
- total_possible_connections += 1
335
-
336
- # 2. Análisis de conectores
337
- connector_count = 0
338
- connector_types = {
339
- 'CCONJ': 1.0, # Coordinantes
340
- 'SCONJ': 1.2, # Subordinantes
341
- 'ADV': 0.8 # Adverbios conectivos
342
- }
343
-
344
- for token in doc:
345
- if (token.pos_ in connector_types and
346
- token.dep_ in ['cc', 'mark', 'advmod'] and
347
- not token.is_stop):
348
- connector_count += connector_types[token.pos_]
349
-
350
- # 3. Cálculo de scores normalizados
351
- if total_possible_connections > 0:
352
- lexical_cohesion = lexical_connections / total_possible_connections
353
- else:
354
- lexical_cohesion = 0
355
-
356
- if len(sentences) > 1:
357
- connector_cohesion = min(1.0, connector_count / (len(sentences) - 1))
358
- else:
359
- connector_cohesion = 0
360
-
361
- # 4. Score final ponderado
362
- weights = {
363
- 'lexical': 0.7,
364
- 'connectors': 0.3
365
- }
366
-
367
- cohesion_score = (
368
- weights['lexical'] * lexical_cohesion +
369
- weights['connectors'] * connector_cohesion
370
- )
371
-
372
- # 5. Logging para diagnóstico
373
- logger.info(f"""
374
- Análisis de Cohesión:
375
- - Conexiones léxicas encontradas: {lexical_connections}
376
- - Conexiones posibles: {total_possible_connections}
377
- - Lexical cohesion score: {lexical_cohesion}
378
- - Conectores encontrados: {connector_count}
379
- - Connector cohesion score: {connector_cohesion}
380
- - Score final: {cohesion_score}
381
- """)
382
-
383
- return cohesion_score
384
-
385
- except Exception as e:
386
- logger.error(f"Error en analyze_cohesion: {str(e)}")
387
- return 0.0
388
-
389
- def analyze_structure(doc):
390
- try:
391
- if len(doc) == 0:
392
- return 0.0
393
-
394
- structure_scores = []
395
- for token in doc:
396
- if token.dep_ == 'ROOT':
397
- result = get_dependency_depths(token)
398
- structure_scores.append(result['final_score'])
399
-
400
- if not structure_scores:
401
- return 0.0
402
-
403
- return min(1.0, sum(structure_scores) / len(structure_scores))
404
-
405
- except Exception as e:
406
- logger.error(f"Error en analyze_structure: {str(e)}")
407
- return 0.0
408
-
409
- # Funciones auxiliares de análisis
410
-
411
- def get_dependency_depths(token, depth=0, analyzed_tokens=None):
412
- """
413
- Analiza la profundidad y calidad de las relaciones de dependencia.
414
-
415
- Args:
416
- token: Token a analizar
417
- depth: Profundidad actual en el árbol
418
- analyzed_tokens: Set para evitar ciclos en el análisis
419
-
420
- Returns:
421
- dict: Información detallada sobre las dependencias
422
- - depths: Lista de profundidades
423
- - relations: Diccionario con tipos de relaciones encontradas
424
- - complexity_score: Puntuación de complejidad
425
- """
426
- if analyzed_tokens is None:
427
- analyzed_tokens = set()
428
-
429
- # Evitar ciclos
430
- if token.i in analyzed_tokens:
431
- return {
432
- 'depths': [],
433
- 'relations': {},
434
- 'complexity_score': 0
435
- }
436
-
437
- analyzed_tokens.add(token.i)
438
-
439
- # Pesos para diferentes tipos de dependencias
440
- dependency_weights = {
441
- # Dependencias principales
442
- 'nsubj': 1.2, # Sujeto nominal
443
- 'obj': 1.1, # Objeto directo
444
- 'iobj': 1.1, # Objeto indirecto
445
- 'ROOT': 1.3, # Raíz
446
-
447
- # Modificadores
448
- 'amod': 0.8, # Modificador adjetival
449
- 'advmod': 0.8, # Modificador adverbial
450
- 'nmod': 0.9, # Modificador nominal
451
-
452
- # Estructuras complejas
453
- 'csubj': 1.4, # Cláusula como sujeto
454
- 'ccomp': 1.3, # Complemento clausal
455
- 'xcomp': 1.2, # Complemento clausal abierto
456
- 'advcl': 1.2, # Cláusula adverbial
457
-
458
- # Coordinación y subordinación
459
- 'conj': 1.1, # Conjunción
460
- 'cc': 0.7, # Coordinación
461
- 'mark': 0.8, # Marcador
462
-
463
- # Otros
464
- 'det': 0.5, # Determinante
465
- 'case': 0.5, # Caso
466
- 'punct': 0.1 # Puntuación
467
- }
468
-
469
- # Inicializar resultados
470
- current_result = {
471
- 'depths': [depth],
472
- 'relations': {token.dep_: 1},
473
- 'complexity_score': dependency_weights.get(token.dep_, 0.5) * (depth + 1)
474
- }
475
-
476
- # Analizar hijos recursivamente
477
- for child in token.children:
478
- child_result = get_dependency_depths(child, depth + 1, analyzed_tokens)
479
-
480
- # Combinar profundidades
481
- current_result['depths'].extend(child_result['depths'])
482
-
483
- # Combinar relaciones
484
- for rel, count in child_result['relations'].items():
485
- current_result['relations'][rel] = current_result['relations'].get(rel, 0) + count
486
-
487
- # Acumular score de complejidad
488
- current_result['complexity_score'] += child_result['complexity_score']
489
-
490
- # Calcular métricas adicionales
491
- current_result['max_depth'] = max(current_result['depths'])
492
- current_result['avg_depth'] = sum(current_result['depths']) / len(current_result['depths'])
493
- current_result['relation_diversity'] = len(current_result['relations'])
494
-
495
- # Calcular score ponderado por tipo de estructura
496
- structure_bonus = 0
497
-
498
- # Bonus por estructuras complejas
499
- if 'csubj' in current_result['relations'] or 'ccomp' in current_result['relations']:
500
- structure_bonus += 0.3
501
-
502
- # Bonus por coordinación balanceada
503
- if 'conj' in current_result['relations'] and 'cc' in current_result['relations']:
504
- structure_bonus += 0.2
505
-
506
- # Bonus por modificación rica
507
- if len(set(['amod', 'advmod', 'nmod']) & set(current_result['relations'])) >= 2:
508
- structure_bonus += 0.2
509
-
510
- current_result['final_score'] = (
511
- current_result['complexity_score'] * (1 + structure_bonus)
512
- )
513
-
514
- return current_result
515
-
516
- def normalize_score(value, metric_type,
517
- min_threshold=0.0, target_threshold=1.0,
518
- range_factor=2.0, optimal_length=None,
519
- optimal_connections=None, optimal_depth=None):
520
- """
521
- Normaliza un valor considerando umbrales específicos por tipo de métrica.
522
-
523
- Args:
524
- value: Valor a normalizar
525
- metric_type: Tipo de métrica ('vocabulary', 'structure', 'cohesion', 'clarity')
526
- min_threshold: Valor mínimo aceptable
527
- target_threshold: Valor objetivo
528
- range_factor: Factor para ajustar el rango
529
- optimal_length: Longitud óptima (opcional)
530
- optimal_connections: Número óptimo de conexiones (opcional)
531
- optimal_depth: Profundidad óptima de estructura (opcional)
532
-
533
- Returns:
534
- float: Valor normalizado entre 0 y 1
535
- """
536
- try:
537
- # Definir umbrales por tipo de métrica
538
- METRIC_THRESHOLDS = {
539
- 'vocabulary': {
540
- 'min': 0.60,
541
- 'target': 0.75,
542
- 'range_factor': 1.5
543
- },
544
- 'structure': {
545
- 'min': 0.65,
546
- 'target': 0.80,
547
- 'range_factor': 1.8
548
- },
549
- 'cohesion': {
550
- 'min': 0.55,
551
- 'target': 0.70,
552
- 'range_factor': 1.6
553
- },
554
- 'clarity': {
555
- 'min': 0.60,
556
- 'target': 0.75,
557
- 'range_factor': 1.7
558
- }
559
- }
560
-
561
- # Validar valores negativos o cero
562
- if value < 0:
563
- logger.warning(f"Valor negativo recibido: {value}")
564
- return 0.0
565
-
566
- # Manejar caso donde el valor es cero
567
- if value == 0:
568
- logger.warning("Valor cero recibido")
569
- return 0.0
570
-
571
- # Obtener umbrales específicos para el tipo de métrica
572
- thresholds = METRIC_THRESHOLDS.get(metric_type, {
573
- 'min': min_threshold,
574
- 'target': target_threshold,
575
- 'range_factor': range_factor
576
- })
577
-
578
- # Identificar el valor de referencia a usar
579
- if optimal_depth is not None:
580
- reference = optimal_depth
581
- elif optimal_connections is not None:
582
- reference = optimal_connections
583
- elif optimal_length is not None:
584
- reference = optimal_length
585
- else:
586
- reference = thresholds['target']
587
-
588
- # Validar valor de referencia
589
- if reference <= 0:
590
- logger.warning(f"Valor de referencia inválido: {reference}")
591
- return 0.0
592
-
593
- # Calcular score basado en umbrales
594
- if value < thresholds['min']:
595
- # Valor por debajo del mínimo
596
- score = (value / thresholds['min']) * 0.5 # Máximo 0.5 para valores bajo el mínimo
597
- elif value < thresholds['target']:
598
- # Valor entre mínimo y objetivo
599
- range_size = thresholds['target'] - thresholds['min']
600
- progress = (value - thresholds['min']) / range_size
601
- score = 0.5 + (progress * 0.5) # Escala entre 0.5 y 1.0
602
- else:
603
- # Valor alcanza o supera el objetivo
604
- score = 1.0
605
-
606
- # Penalizar valores muy por encima del objetivo
607
- if value > (thresholds['target'] * thresholds['range_factor']):
608
- excess = (value - thresholds['target']) / (thresholds['target'] * thresholds['range_factor'])
609
- score = max(0.7, 1.0 - excess) # No bajar de 0.7 para valores altos
610
-
611
- # Asegurar que el resultado esté entre 0 y 1
612
- return max(0.0, min(1.0, score))
613
-
614
- except Exception as e:
615
- logger.error(f"Error en normalize_score: {str(e)}")
616
- return 0.0
617
-
618
-
619
- # Funciones de generación de gráficos
620
- def generate_sentence_graphs(doc):
621
- """Genera visualizaciones de estructura de oraciones"""
622
- fig, ax = plt.subplots(figsize=(10, 6))
623
- # Implementar visualización
624
- plt.close()
625
- return fig
626
-
627
- def generate_word_connections(doc):
628
- """Genera red de conexiones de palabras"""
629
- fig, ax = plt.subplots(figsize=(10, 6))
630
- # Implementar visualización
631
- plt.close()
632
- return fig
633
-
634
- def generate_connection_paths(doc):
635
- """Genera patrones de conexión"""
636
- fig, ax = plt.subplots(figsize=(10, 6))
637
- # Implementar visualización
638
- plt.close()
639
- return fig
640
-
641
- def create_vocabulary_network(doc):
642
- """
643
- Genera el grafo de red de vocabulario.
644
- """
645
- G = nx.Graph()
646
-
647
- # Crear nodos para palabras significativas
648
- words = [token.text.lower() for token in doc if token.is_alpha and not token.is_stop]
649
- word_freq = Counter(words)
650
-
651
- # Añadir nodos con tamaño basado en frecuencia
652
- for word, freq in word_freq.items():
653
- G.add_node(word, size=freq)
654
-
655
- # Crear conexiones basadas en co-ocurrencia
656
- window_size = 5
657
- for i in range(len(words) - window_size):
658
- window = words[i:i+window_size]
659
- for w1, w2 in combinations(set(window), 2):
660
- if G.has_edge(w1, w2):
661
- G[w1][w2]['weight'] += 1
662
- else:
663
- G.add_edge(w1, w2, weight=1)
664
-
665
- # Crear visualización
666
- fig, ax = plt.subplots(figsize=(12, 8))
667
- pos = nx.spring_layout(G)
668
-
669
- # Dibujar nodos
670
- nx.draw_networkx_nodes(G, pos,
671
- node_size=[G.nodes[node]['size']*100 for node in G.nodes],
672
- node_color='lightblue',
673
- alpha=0.7)
674
-
675
- # Dibujar conexiones
676
- nx.draw_networkx_edges(G, pos,
677
- width=[G[u][v]['weight']*0.5 for u,v in G.edges],
678
- alpha=0.5)
679
-
680
- # Añadir etiquetas
681
- nx.draw_networkx_labels(G, pos)
682
-
683
- plt.title("Red de Vocabulario")
684
- plt.axis('off')
685
- return fig
686
-
687
- def create_syntax_complexity_graph(doc):
688
- """
689
- Genera el diagrama de arco de complejidad sintáctica.
690
- Muestra la estructura de dependencias con colores basados en la complejidad.
691
- """
692
- try:
693
- # Preparar datos para la visualización
694
- sentences = list(doc.sents)
695
- if not sentences:
696
- return None
697
-
698
- # Crear figura para el gráfico
699
- fig, ax = plt.subplots(figsize=(12, len(sentences) * 2))
700
-
701
- # Colores para diferentes niveles de profundidad
702
- depth_colors = plt.cm.viridis(np.linspace(0, 1, 6))
703
-
704
- y_offset = 0
705
- max_x = 0
706
-
707
- for sent in sentences:
708
- words = [token.text for token in sent]
709
- x_positions = range(len(words))
710
- max_x = max(max_x, len(words))
711
-
712
- # Dibujar palabras
713
- plt.plot(x_positions, [y_offset] * len(words), 'k-', alpha=0.2)
714
- plt.scatter(x_positions, [y_offset] * len(words), alpha=0)
715
-
716
- # Añadir texto
717
- for i, word in enumerate(words):
718
- plt.annotate(word, (i, y_offset), xytext=(0, -10),
719
- textcoords='offset points', ha='center')
720
-
721
- # Dibujar arcos de dependencia
722
- for token in sent:
723
- if token.dep_ != "ROOT":
724
- # Calcular profundidad de dependencia
725
- depth = 0
726
- current = token
727
- while current.head != current:
728
- depth += 1
729
- current = current.head
730
-
731
- # Determinar posiciones para el arco
732
- start = token.i - sent[0].i
733
- end = token.head.i - sent[0].i
734
-
735
- # Altura del arco basada en la distancia entre palabras
736
- height = 0.5 * abs(end - start)
737
-
738
- # Color basado en la profundidad
739
- color = depth_colors[min(depth, len(depth_colors)-1)]
740
-
741
- # Crear arco
742
- arc = patches.Arc((min(start, end) + abs(end - start)/2, y_offset),
743
- width=abs(end - start),
744
- height=height,
745
- angle=0,
746
- theta1=0,
747
- theta2=180,
748
- color=color,
749
- alpha=0.6)
750
- ax.add_patch(arc)
751
-
752
- y_offset -= 2
753
-
754
- # Configurar el gráfico
755
- plt.xlim(-1, max_x)
756
- plt.ylim(y_offset - 1, 1)
757
- plt.axis('off')
758
- plt.title("Complejidad Sintáctica")
759
-
760
- return fig
761
-
762
- except Exception as e:
763
- logger.error(f"Error en create_syntax_complexity_graph: {str(e)}")
764
- return None
765
-
766
-
767
- def create_cohesion_heatmap(doc):
768
- """Genera un mapa de calor que muestra la cohesión entre párrafos/oraciones."""
769
- try:
770
- sentences = list(doc.sents)
771
- n_sentences = len(sentences)
772
-
773
- if n_sentences < 2:
774
- return None
775
-
776
- similarity_matrix = np.zeros((n_sentences, n_sentences))
777
-
778
- for i in range(n_sentences):
779
- for j in range(n_sentences):
780
- sent1_lemmas = {token.lemma_ for token in sentences[i]
781
- if token.is_alpha and not token.is_stop}
782
- sent2_lemmas = {token.lemma_ for token in sentences[j]
783
- if token.is_alpha and not token.is_stop}
784
-
785
- if sent1_lemmas and sent2_lemmas:
786
- intersection = len(sent1_lemmas & sent2_lemmas) # Corregido aquí
787
- union = len(sent1_lemmas | sent2_lemmas) # Y aquí
788
- similarity_matrix[i, j] = intersection / union if union > 0 else 0
789
-
790
- # Crear visualización
791
- fig, ax = plt.subplots(figsize=(10, 8))
792
-
793
- sns.heatmap(similarity_matrix,
794
- cmap='YlOrRd',
795
- square=True,
796
- xticklabels=False,
797
- yticklabels=False,
798
- cbar_kws={'label': 'Cohesión'},
799
- ax=ax)
800
-
801
- plt.title("Mapa de Cohesión Textual")
802
- plt.xlabel("Oraciones")
803
- plt.ylabel("Oraciones")
804
-
805
- plt.tight_layout()
806
- return fig
807
-
808
- except Exception as e:
809
- logger.error(f"Error en create_cohesion_heatmap: {str(e)}")
810
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #v3/modules/studentact/current_situation_analysis.py
2
+
3
+ import streamlit as st
4
+ import matplotlib.pyplot as plt
5
+ import networkx as nx
6
+ import seaborn as sns
7
+ from collections import Counter
8
+ from itertools import combinations
9
+ import numpy as np
10
+ import matplotlib.patches as patches
11
+ import logging
12
+
13
+ from translations.recommendations import RECOMMENDATIONS
14
+
15
+ # 2. Configuración básica del logging
16
+ logging.basicConfig(
17
+ level=logging.INFO,
18
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
19
+ handlers=[
20
+ logging.StreamHandler(),
21
+ logging.FileHandler('app.log')
22
+ ]
23
+ )
24
+
25
+ # 3. Obtener el logger específico para este módulo
26
+ logger = logging.getLogger(__name__)
27
+
28
+ #########################################################################
29
+
30
+ def correlate_metrics(scores):
31
+ """
32
+ Ajusta los scores para mantener correlaciones lógicas entre métricas.
33
+
34
+ Args:
35
+ scores: dict con scores iniciales de vocabulario, estructura, cohesión y claridad
36
+
37
+ Returns:
38
+ dict con scores ajustados
39
+ """
40
+ try:
41
+ # 1. Correlación estructura-cohesión
42
+ # La cohesión no puede ser menor que estructura * 0.7
43
+ min_cohesion = scores['structure']['normalized_score'] * 0.7
44
+ if scores['cohesion']['normalized_score'] < min_cohesion:
45
+ scores['cohesion']['normalized_score'] = min_cohesion
46
+
47
+ # 2. Correlación vocabulario-cohesión
48
+ # La cohesión léxica depende del vocabulario
49
+ vocab_influence = scores['vocabulary']['normalized_score'] * 0.6
50
+ scores['cohesion']['normalized_score'] = max(
51
+ scores['cohesion']['normalized_score'],
52
+ vocab_influence
53
+ )
54
+
55
+ # 3. Correlación cohesión-claridad
56
+ # La claridad no puede superar cohesión * 1.2
57
+ max_clarity = scores['cohesion']['normalized_score'] * 1.2
58
+ if scores['clarity']['normalized_score'] > max_clarity:
59
+ scores['clarity']['normalized_score'] = max_clarity
60
+
61
+ # 4. Correlación estructura-claridad
62
+ # La claridad no puede superar estructura * 1.1
63
+ struct_max_clarity = scores['structure']['normalized_score'] * 1.1
64
+ scores['clarity']['normalized_score'] = min(
65
+ scores['clarity']['normalized_score'],
66
+ struct_max_clarity
67
+ )
68
+
69
+ # Normalizar todos los scores entre 0 y 1
70
+ for metric in scores:
71
+ scores[metric]['normalized_score'] = max(0.0, min(1.0, scores[metric]['normalized_score']))
72
+
73
+ return scores
74
+
75
+ except Exception as e:
76
+ logger.error(f"Error en correlate_metrics: {str(e)}")
77
+ return scores
78
+
79
+ ##########################################################################
80
+
81
+ def analyze_text_dimensions(doc):
82
+ """
83
+ Analiza las dimensiones principales del texto manteniendo correlaciones lógicas.
84
+ """
85
+ try:
86
+ # Obtener scores iniciales
87
+ vocab_score, vocab_details = analyze_vocabulary_diversity(doc)
88
+ struct_score = analyze_structure(doc)
89
+ cohesion_score = analyze_cohesion(doc)
90
+ clarity_score, clarity_details = analyze_clarity(doc)
91
+
92
+ # Crear diccionario de scores inicial
93
+ scores = {
94
+ 'vocabulary': {
95
+ 'normalized_score': vocab_score,
96
+ 'details': vocab_details
97
+ },
98
+ 'structure': {
99
+ 'normalized_score': struct_score,
100
+ 'details': None
101
+ },
102
+ 'cohesion': {
103
+ 'normalized_score': cohesion_score,
104
+ 'details': None
105
+ },
106
+ 'clarity': {
107
+ 'normalized_score': clarity_score,
108
+ 'details': clarity_details
109
+ }
110
+ }
111
+
112
+ # Ajustar correlaciones entre métricas
113
+ adjusted_scores = correlate_metrics(scores)
114
+
115
+ # Logging para diagnóstico
116
+ logger.info(f"""
117
+ Scores originales vs ajustados:
118
+ Vocabulario: {vocab_score:.2f} -> {adjusted_scores['vocabulary']['normalized_score']:.2f}
119
+ Estructura: {struct_score:.2f} -> {adjusted_scores['structure']['normalized_score']:.2f}
120
+ Cohesión: {cohesion_score:.2f} -> {adjusted_scores['cohesion']['normalized_score']:.2f}
121
+ Claridad: {clarity_score:.2f} -> {adjusted_scores['clarity']['normalized_score']:.2f}
122
+ """)
123
+
124
+ return adjusted_scores
125
+
126
+ except Exception as e:
127
+ logger.error(f"Error en analyze_text_dimensions: {str(e)}")
128
+ return {
129
+ 'vocabulary': {'normalized_score': 0.0, 'details': {}},
130
+ 'structure': {'normalized_score': 0.0, 'details': {}},
131
+ 'cohesion': {'normalized_score': 0.0, 'details': {}},
132
+ 'clarity': {'normalized_score': 0.0, 'details': {}}
133
+ }
134
+
135
+
136
+
137
+ #############################################################################################
138
+
139
+ def analyze_clarity(doc):
140
+ """
141
+ Analiza la claridad del texto considerando múltiples factores.
142
+ """
143
+ try:
144
+ sentences = list(doc.sents)
145
+ if not sentences:
146
+ return 0.0, {}
147
+
148
+ # 1. Longitud de oraciones
149
+ sentence_lengths = [len(sent) for sent in sentences]
150
+ avg_length = sum(sentence_lengths) / len(sentences)
151
+
152
+ # Normalizar usando los umbrales definidos para clarity
153
+ length_score = normalize_score(
154
+ value=avg_length,
155
+ metric_type='clarity',
156
+ optimal_length=20, # Una oración ideal tiene ~20 palabras
157
+ min_threshold=0.60, # Consistente con METRIC_THRESHOLDS
158
+ target_threshold=0.75 # Consistente con METRIC_THRESHOLDS
159
+ )
160
+
161
+ # 2. Análisis de conectores
162
+ connector_count = 0
163
+ connector_weights = {
164
+ 'CCONJ': 1.0, # Coordinantes
165
+ 'SCONJ': 1.2, # Subordinantes
166
+ 'ADV': 0.8 # Adverbios conectivos
167
+ }
168
+
169
+ for token in doc:
170
+ if token.pos_ in connector_weights and token.dep_ in ['cc', 'mark', 'advmod']:
171
+ connector_count += connector_weights[token.pos_]
172
+
173
+ # Normalizar conectores por oración
174
+ connectors_per_sentence = connector_count / len(sentences) if sentences else 0
175
+ connector_score = normalize_score(
176
+ value=connectors_per_sentence,
177
+ metric_type='clarity',
178
+ optimal_connections=1.5, # ~1.5 conectores por oración es óptimo
179
+ min_threshold=0.60,
180
+ target_threshold=0.75
181
+ )
182
+
183
+ # 3. Complejidad estructural
184
+ clause_count = 0
185
+ for sent in sentences:
186
+ verbs = [token for token in sent if token.pos_ == 'VERB']
187
+ clause_count += len(verbs)
188
+
189
+ complexity_raw = clause_count / len(sentences) if sentences else 0
190
+ complexity_score = normalize_score(
191
+ value=complexity_raw,
192
+ metric_type='clarity',
193
+ optimal_depth=2.0, # ~2 cláusulas por oración es óptimo
194
+ min_threshold=0.60,
195
+ target_threshold=0.75
196
+ )
197
+
198
+ # 4. Densidad léxica
199
+ content_words = len([token for token in doc if token.pos_ in ['NOUN', 'VERB', 'ADJ', 'ADV']])
200
+ total_words = len([token for token in doc if token.is_alpha])
201
+ density = content_words / total_words if total_words > 0 else 0
202
+
203
+ density_score = normalize_score(
204
+ value=density,
205
+ metric_type='clarity',
206
+ optimal_connections=0.6, # 60% de palabras de contenido es óptimo
207
+ min_threshold=0.60,
208
+ target_threshold=0.75
209
+ )
210
+
211
+ # Score final ponderado
212
+ weights = {
213
+ 'length': 0.3,
214
+ 'connectors': 0.3,
215
+ 'complexity': 0.2,
216
+ 'density': 0.2
217
+ }
218
+
219
+ clarity_score = (
220
+ weights['length'] * length_score +
221
+ weights['connectors'] * connector_score +
222
+ weights['complexity'] * complexity_score +
223
+ weights['density'] * density_score
224
+ )
225
+
226
+ details = {
227
+ 'length_score': length_score,
228
+ 'connector_score': connector_score,
229
+ 'complexity_score': complexity_score,
230
+ 'density_score': density_score,
231
+ 'avg_sentence_length': avg_length,
232
+ 'connectors_per_sentence': connectors_per_sentence,
233
+ 'density': density
234
+ }
235
+
236
+ # Agregar logging para diagnóstico
237
+ logger.info(f"""
238
+ Scores de Claridad:
239
+ - Longitud: {length_score:.2f} (avg={avg_length:.1f} palabras)
240
+ - Conectores: {connector_score:.2f} (avg={connectors_per_sentence:.1f} por oración)
241
+ - Complejidad: {complexity_score:.2f} (avg={complexity_raw:.1f} cláusulas)
242
+ - Densidad: {density_score:.2f} ({density*100:.1f}% palabras de contenido)
243
+ - Score Final: {clarity_score:.2f}
244
+ """)
245
+
246
+ return clarity_score, details
247
+
248
+ except Exception as e:
249
+ logger.error(f"Error en analyze_clarity: {str(e)}")
250
+ return 0.0, {}
251
+
252
+ #########################################################################
253
+ def analyze_vocabulary_diversity(doc):
254
+ """Análisis mejorado de la diversidad y calidad del vocabulario"""
255
+ try:
256
+ # 1. Análisis básico de diversidad
257
+ unique_lemmas = {token.lemma_ for token in doc if token.is_alpha}
258
+ total_words = len([token for token in doc if token.is_alpha])
259
+ basic_diversity = len(unique_lemmas) / total_words if total_words > 0 else 0
260
+
261
+ # 2. Análisis de registro
262
+ academic_words = 0
263
+ narrative_words = 0
264
+ technical_terms = 0
265
+
266
+ # Clasificar palabras por registro
267
+ for token in doc:
268
+ if token.is_alpha:
269
+ # Detectar términos académicos/técnicos
270
+ if token.pos_ in ['NOUN', 'VERB', 'ADJ']:
271
+ if any(parent.pos_ == 'NOUN' for parent in token.ancestors):
272
+ technical_terms += 1
273
+ # Detectar palabras narrativas
274
+ if token.pos_ in ['VERB', 'ADV'] and token.dep_ in ['ROOT', 'advcl']:
275
+ narrative_words += 1
276
+
277
+ # 3. Análisis de complejidad sintáctica
278
+ avg_sentence_length = sum(len(sent) for sent in doc.sents) / len(list(doc.sents))
279
+
280
+ # 4. Calcular score ponderado
281
+ weights = {
282
+ 'diversity': 0.3,
283
+ 'technical': 0.3,
284
+ 'narrative': 0.2,
285
+ 'complexity': 0.2
286
+ }
287
+
288
+ scores = {
289
+ 'diversity': basic_diversity,
290
+ 'technical': technical_terms / total_words if total_words > 0 else 0,
291
+ 'narrative': narrative_words / total_words if total_words > 0 else 0,
292
+ 'complexity': min(1.0, avg_sentence_length / 20) # Normalizado a 20 palabras
293
+ }
294
+
295
+ # Score final ponderado
296
+ final_score = sum(weights[key] * scores[key] for key in weights)
297
+
298
+ # Información adicional para diagnóstico
299
+ details = {
300
+ 'text_type': 'narrative' if scores['narrative'] > scores['technical'] else 'academic',
301
+ 'scores': scores
302
+ }
303
+
304
+ return final_score, details
305
+
306
+ except Exception as e:
307
+ logger.error(f"Error en analyze_vocabulary_diversity: {str(e)}")
308
+ return 0.0, {}
309
+
310
+ #########################################################################
311
+ def analyze_cohesion(doc):
312
+ """Analiza la cohesión textual"""
313
+ try:
314
+ sentences = list(doc.sents)
315
+ if len(sentences) < 2:
316
+ logger.warning("Texto demasiado corto para análisis de cohesión")
317
+ return 0.0
318
+
319
+ # 1. Análisis de conexiones léxicas
320
+ lexical_connections = 0
321
+ total_possible_connections = 0
322
+
323
+ for i in range(len(sentences)-1):
324
+ # Obtener lemmas significativos (no stopwords)
325
+ sent1_words = {token.lemma_ for token in sentences[i]
326
+ if token.is_alpha and not token.is_stop}
327
+ sent2_words = {token.lemma_ for token in sentences[i+1]
328
+ if token.is_alpha and not token.is_stop}
329
+
330
+ if sent1_words and sent2_words: # Verificar que ambos conjuntos no estén vacíos
331
+ intersection = len(sent1_words.intersection(sent2_words))
332
+ total_possible = min(len(sent1_words), len(sent2_words))
333
+
334
+ if total_possible > 0:
335
+ lexical_score = intersection / total_possible
336
+ lexical_connections += lexical_score
337
+ total_possible_connections += 1
338
+
339
+ # 2. Análisis de conectores
340
+ connector_count = 0
341
+ connector_types = {
342
+ 'CCONJ': 1.0, # Coordinantes
343
+ 'SCONJ': 1.2, # Subordinantes
344
+ 'ADV': 0.8 # Adverbios conectivos
345
+ }
346
+
347
+ for token in doc:
348
+ if (token.pos_ in connector_types and
349
+ token.dep_ in ['cc', 'mark', 'advmod'] and
350
+ not token.is_stop):
351
+ connector_count += connector_types[token.pos_]
352
+
353
+ # 3. Cálculo de scores normalizados
354
+ if total_possible_connections > 0:
355
+ lexical_cohesion = lexical_connections / total_possible_connections
356
+ else:
357
+ lexical_cohesion = 0
358
+
359
+ if len(sentences) > 1:
360
+ connector_cohesion = min(1.0, connector_count / (len(sentences) - 1))
361
+ else:
362
+ connector_cohesion = 0
363
+
364
+ # 4. Score final ponderado
365
+ weights = {
366
+ 'lexical': 0.7,
367
+ 'connectors': 0.3
368
+ }
369
+
370
+ cohesion_score = (
371
+ weights['lexical'] * lexical_cohesion +
372
+ weights['connectors'] * connector_cohesion
373
+ )
374
+
375
+ # 5. Logging para diagnóstico
376
+ logger.info(f"""
377
+ Análisis de Cohesión:
378
+ - Conexiones léxicas encontradas: {lexical_connections}
379
+ - Conexiones posibles: {total_possible_connections}
380
+ - Lexical cohesion score: {lexical_cohesion}
381
+ - Conectores encontrados: {connector_count}
382
+ - Connector cohesion score: {connector_cohesion}
383
+ - Score final: {cohesion_score}
384
+ """)
385
+
386
+ return cohesion_score
387
+
388
+ except Exception as e:
389
+ logger.error(f"Error en analyze_cohesion: {str(e)}")
390
+ return 0.0
391
+
392
+ #########################################################################
393
+ def analyze_structure(doc):
394
+ try:
395
+ if len(doc) == 0:
396
+ return 0.0
397
+
398
+ structure_scores = []
399
+ for token in doc:
400
+ if token.dep_ == 'ROOT':
401
+ result = get_dependency_depths(token)
402
+ structure_scores.append(result['final_score'])
403
+
404
+ if not structure_scores:
405
+ return 0.0
406
+
407
+ return min(1.0, sum(structure_scores) / len(structure_scores))
408
+
409
+ except Exception as e:
410
+ logger.error(f"Error en analyze_structure: {str(e)}")
411
+ return 0.0
412
+
413
+ #########################################################################
414
+ # Funciones auxiliares de análisis
415
+ def get_dependency_depths(token, depth=0, analyzed_tokens=None):
416
+ """
417
+ Analiza la profundidad y calidad de las relaciones de dependencia.
418
+
419
+ Args:
420
+ token: Token a analizar
421
+ depth: Profundidad actual en el árbol
422
+ analyzed_tokens: Set para evitar ciclos en el análisis
423
+
424
+ Returns:
425
+ dict: Información detallada sobre las dependencias
426
+ - depths: Lista de profundidades
427
+ - relations: Diccionario con tipos de relaciones encontradas
428
+ - complexity_score: Puntuación de complejidad
429
+ """
430
+ if analyzed_tokens is None:
431
+ analyzed_tokens = set()
432
+
433
+ # Evitar ciclos
434
+ if token.i in analyzed_tokens:
435
+ return {
436
+ 'depths': [],
437
+ 'relations': {},
438
+ 'complexity_score': 0
439
+ }
440
+
441
+ analyzed_tokens.add(token.i)
442
+
443
+ # Pesos para diferentes tipos de dependencias
444
+ dependency_weights = {
445
+ # Dependencias principales
446
+ 'nsubj': 1.2, # Sujeto nominal
447
+ 'obj': 1.1, # Objeto directo
448
+ 'iobj': 1.1, # Objeto indirecto
449
+ 'ROOT': 1.3, # Raíz
450
+
451
+ # Modificadores
452
+ 'amod': 0.8, # Modificador adjetival
453
+ 'advmod': 0.8, # Modificador adverbial
454
+ 'nmod': 0.9, # Modificador nominal
455
+
456
+ # Estructuras complejas
457
+ 'csubj': 1.4, # Cláusula como sujeto
458
+ 'ccomp': 1.3, # Complemento clausal
459
+ 'xcomp': 1.2, # Complemento clausal abierto
460
+ 'advcl': 1.2, # Cláusula adverbial
461
+
462
+ # Coordinación y subordinación
463
+ 'conj': 1.1, # Conjunción
464
+ 'cc': 0.7, # Coordinación
465
+ 'mark': 0.8, # Marcador
466
+
467
+ # Otros
468
+ 'det': 0.5, # Determinante
469
+ 'case': 0.5, # Caso
470
+ 'punct': 0.1 # Puntuación
471
+ }
472
+
473
+ # Inicializar resultados
474
+ current_result = {
475
+ 'depths': [depth],
476
+ 'relations': {token.dep_: 1},
477
+ 'complexity_score': dependency_weights.get(token.dep_, 0.5) * (depth + 1)
478
+ }
479
+
480
+ # Analizar hijos recursivamente
481
+ for child in token.children:
482
+ child_result = get_dependency_depths(child, depth + 1, analyzed_tokens)
483
+
484
+ # Combinar profundidades
485
+ current_result['depths'].extend(child_result['depths'])
486
+
487
+ # Combinar relaciones
488
+ for rel, count in child_result['relations'].items():
489
+ current_result['relations'][rel] = current_result['relations'].get(rel, 0) + count
490
+
491
+ # Acumular score de complejidad
492
+ current_result['complexity_score'] += child_result['complexity_score']
493
+
494
+ # Calcular métricas adicionales
495
+ current_result['max_depth'] = max(current_result['depths'])
496
+ current_result['avg_depth'] = sum(current_result['depths']) / len(current_result['depths'])
497
+ current_result['relation_diversity'] = len(current_result['relations'])
498
+
499
+ # Calcular score ponderado por tipo de estructura
500
+ structure_bonus = 0
501
+
502
+ # Bonus por estructuras complejas
503
+ if 'csubj' in current_result['relations'] or 'ccomp' in current_result['relations']:
504
+ structure_bonus += 0.3
505
+
506
+ # Bonus por coordinación balanceada
507
+ if 'conj' in current_result['relations'] and 'cc' in current_result['relations']:
508
+ structure_bonus += 0.2
509
+
510
+ # Bonus por modificación rica
511
+ if len(set(['amod', 'advmod', 'nmod']) & set(current_result['relations'])) >= 2:
512
+ structure_bonus += 0.2
513
+
514
+ current_result['final_score'] = (
515
+ current_result['complexity_score'] * (1 + structure_bonus)
516
+ )
517
+
518
+ return current_result
519
+
520
+ #########################################################################
521
+ def normalize_score(value, metric_type,
522
+ min_threshold=0.0, target_threshold=1.0,
523
+ range_factor=2.0, optimal_length=None,
524
+ optimal_connections=None, optimal_depth=None):
525
+ """
526
+ Normaliza un valor considerando umbrales específicos por tipo de métrica.
527
+
528
+ Args:
529
+ value: Valor a normalizar
530
+ metric_type: Tipo de métrica ('vocabulary', 'structure', 'cohesion', 'clarity')
531
+ min_threshold: Valor mínimo aceptable
532
+ target_threshold: Valor objetivo
533
+ range_factor: Factor para ajustar el rango
534
+ optimal_length: Longitud óptima (opcional)
535
+ optimal_connections: Número óptimo de conexiones (opcional)
536
+ optimal_depth: Profundidad óptima de estructura (opcional)
537
+
538
+ Returns:
539
+ float: Valor normalizado entre 0 y 1
540
+ """
541
+ try:
542
+ # Definir umbrales por tipo de métrica
543
+ METRIC_THRESHOLDS = {
544
+ 'vocabulary': {
545
+ 'min': 0.60,
546
+ 'target': 0.75,
547
+ 'range_factor': 1.5
548
+ },
549
+ 'structure': {
550
+ 'min': 0.65,
551
+ 'target': 0.80,
552
+ 'range_factor': 1.8
553
+ },
554
+ 'cohesion': {
555
+ 'min': 0.55,
556
+ 'target': 0.70,
557
+ 'range_factor': 1.6
558
+ },
559
+ 'clarity': {
560
+ 'min': 0.60,
561
+ 'target': 0.75,
562
+ 'range_factor': 1.7
563
+ }
564
+ }
565
+
566
+ # Validar valores negativos o cero
567
+ if value < 0:
568
+ logger.warning(f"Valor negativo recibido: {value}")
569
+ return 0.0
570
+
571
+ # Manejar caso donde el valor es cero
572
+ if value == 0:
573
+ logger.warning("Valor cero recibido")
574
+ return 0.0
575
+
576
+ # Obtener umbrales específicos para el tipo de métrica
577
+ thresholds = METRIC_THRESHOLDS.get(metric_type, {
578
+ 'min': min_threshold,
579
+ 'target': target_threshold,
580
+ 'range_factor': range_factor
581
+ })
582
+
583
+ # Identificar el valor de referencia a usar
584
+ if optimal_depth is not None:
585
+ reference = optimal_depth
586
+ elif optimal_connections is not None:
587
+ reference = optimal_connections
588
+ elif optimal_length is not None:
589
+ reference = optimal_length
590
+ else:
591
+ reference = thresholds['target']
592
+
593
+ # Validar valor de referencia
594
+ if reference <= 0:
595
+ logger.warning(f"Valor de referencia inválido: {reference}")
596
+ return 0.0
597
+
598
+ # Calcular score basado en umbrales
599
+ if value < thresholds['min']:
600
+ # Valor por debajo del mínimo
601
+ score = (value / thresholds['min']) * 0.5 # Máximo 0.5 para valores bajo el mínimo
602
+ elif value < thresholds['target']:
603
+ # Valor entre mínimo y objetivo
604
+ range_size = thresholds['target'] - thresholds['min']
605
+ progress = (value - thresholds['min']) / range_size
606
+ score = 0.5 + (progress * 0.5) # Escala entre 0.5 y 1.0
607
+ else:
608
+ # Valor alcanza o supera el objetivo
609
+ score = 1.0
610
+
611
+ # Penalizar valores muy por encima del objetivo
612
+ if value > (thresholds['target'] * thresholds['range_factor']):
613
+ excess = (value - thresholds['target']) / (thresholds['target'] * thresholds['range_factor'])
614
+ score = max(0.7, 1.0 - excess) # No bajar de 0.7 para valores altos
615
+
616
+ # Asegurar que el resultado esté entre 0 y 1
617
+ return max(0.0, min(1.0, score))
618
+
619
+ except Exception as e:
620
+ logger.error(f"Error en normalize_score: {str(e)}")
621
+ return 0.0
622
+
623
+ #########################################################################
624
+ #########################################################################
625
+ def generate_recommendations(metrics, text_type, lang_code='es'):
626
+ """
627
+ Genera recomendaciones personalizadas basadas en las métricas del texto y el tipo de texto.
628
+
629
+ Args:
630
+ metrics: Diccionario con las métricas analizadas
631
+ text_type: Tipo de texto ('academic_article', 'student_essay', 'general_communication')
632
+ lang_code: Código del idioma para las recomendaciones (es, en, fr, pt)
633
+
634
+ Returns:
635
+ dict: Recomendaciones organizadas por categoría en el idioma correspondiente
636
+ """
637
+ try:
638
+ # Obtener umbrales según el tipo de texto
639
+ thresholds = TEXT_TYPES[text_type]['thresholds']
640
+
641
+ # Verificar que el idioma esté soportado, usar español como respaldo
642
+ if lang_code not in RECOMMENDATIONS:
643
+ logger.warning(f"Idioma {lang_code} no soportado para recomendaciones, usando español")
644
+ lang_code = 'es'
645
+
646
+ # Obtener traducciones para el idioma seleccionado
647
+ translations = RECOMMENDATIONS[lang_code]
648
+
649
+ # Inicializar diccionario de recomendaciones
650
+ recommendations = {
651
+ 'vocabulary': [],
652
+ 'structure': [],
653
+ 'cohesion': [],
654
+ 'clarity': [],
655
+ 'specific': [],
656
+ 'priority': {
657
+ 'area': 'general',
658
+ 'tips': []
659
+ },
660
+ 'text_type_name': translations['text_types'][text_type],
661
+ 'dimension_names': translations['dimension_names'],
662
+ 'ui_text': {
663
+ 'priority_intro': translations['priority_intro'],
664
+ 'detailed_recommendations': translations['detailed_recommendations'],
665
+ 'save_button': translations['save_button'],
666
+ 'save_success': translations['save_success'],
667
+ 'save_error': translations['save_error'],
668
+ 'area_priority': translations['area_priority']
669
+ }
670
+ }
671
+
672
+ # Determinar nivel para cada dimensión y asignar recomendaciones
673
+ dimensions = ['vocabulary', 'structure', 'cohesion', 'clarity']
674
+ scores = {}
675
+
676
+ for dim in dimensions:
677
+ score = metrics[dim]['normalized_score']
678
+ scores[dim] = score
679
+
680
+ # Determinar nivel (bajo, medio, alto)
681
+ if score < thresholds[dim]['min']:
682
+ level = 'low'
683
+ elif score < thresholds[dim]['target']:
684
+ level = 'medium'
685
+ else:
686
+ level = 'high'
687
+
688
+ # Asignar recomendaciones para ese nivel
689
+ recommendations[dim] = translations[dim][level]
690
+
691
+ # Asignar recomendaciones específicas por tipo de texto
692
+ recommendations['specific'] = translations[text_type]
693
+
694
+ # Determinar área prioritaria (la que tiene menor puntuación)
695
+ priority_dimension = min(scores, key=scores.get)
696
+ recommendations['priority']['area'] = priority_dimension
697
+ recommendations['priority']['tips'] = recommendations[priority_dimension]
698
+
699
+ logger.info(f"Generadas recomendaciones en {lang_code} para texto tipo {text_type}")
700
+ return recommendations
701
+
702
+ except Exception as e:
703
+ logger.error(f"Error en generate_recommendations: {str(e)}")
704
+ # Retornar mensajes genéricos en caso de error
705
+ if lang_code == 'en':
706
+ return {
707
+ 'vocabulary': ["Try enriching your vocabulary"],
708
+ 'structure': ["Work on the structure of your sentences"],
709
+ 'cohesion': ["Improve the connection between your ideas"],
710
+ 'clarity': ["Try to express your ideas more clearly"],
711
+ 'specific': ["Adapt your text according to its purpose"],
712
+ 'priority': {
713
+ 'area': 'general',
714
+ 'tips': ["Seek specific feedback from a tutor or teacher"]
715
+ },
716
+ 'dimension_names': {
717
+ 'vocabulary': 'Vocabulary',
718
+ 'structure': 'Structure',
719
+ 'cohesion': 'Cohesion',
720
+ 'clarity': 'Clarity',
721
+ 'general': 'General'
722
+ },
723
+ 'ui_text': {
724
+ 'priority_intro': "This is where you should focus your efforts.",
725
+ 'detailed_recommendations': "Detailed recommendations",
726
+ 'save_button': "Save analysis",
727
+ 'save_success': "Analysis saved successfully",
728
+ 'save_error': "Error saving analysis",
729
+ 'area_priority': "Priority area"
730
+ }
731
+ }
732
+ elif lang_code == 'fr':
733
+ return {
734
+ 'vocabulary': ["Essayez d'enrichir votre vocabulaire"],
735
+ 'structure': ["Travaillez sur la structure de vos phrases"],
736
+ 'cohesion': ["Améliorez la connexion entre vos idées"],
737
+ 'clarity': ["Essayez d'exprimer vos idées plus clairement"],
738
+ 'specific': ["Adaptez votre texte en fonction de son objectif"],
739
+ 'priority': {
740
+ 'area': 'general',
741
+ 'tips': ["Demandez des commentaires spécifiques à un tuteur ou un professeur"]
742
+ },
743
+ 'dimension_names': {
744
+ 'vocabulary': 'Vocabulaire',
745
+ 'structure': 'Structure',
746
+ 'cohesion': 'Cohésion',
747
+ 'clarity': 'Clarté',
748
+ 'general': 'Général'
749
+ },
750
+ 'ui_text': {
751
+ 'priority_intro': "C'est là que vous devriez concentrer vos efforts.",
752
+ 'detailed_recommendations': "Recommandations détaillées",
753
+ 'save_button': "Enregistrer l'analyse",
754
+ 'save_success': "Analyse enregistrée avec succès",
755
+ 'save_error': "Erreur lors de l'enregistrement de l'analyse",
756
+ 'area_priority': "Domaine prioritaire"
757
+ }
758
+ }
759
+ elif lang_code == 'pt':
760
+ return {
761
+ 'vocabulary': ["Tente enriquecer seu vocabulário"],
762
+ 'structure': ["Trabalhe na estrutura de suas frases"],
763
+ 'cohesion': ["Melhore a conexão entre suas ideias"],
764
+ 'clarity': ["Tente expressar suas ideias com mais clareza"],
765
+ 'specific': ["Adapte seu texto de acordo com seu propósito"],
766
+ 'priority': {
767
+ 'area': 'general',
768
+ 'tips': ["Busque feedback específico de um tutor ou professor"]
769
+ },
770
+ 'dimension_names': {
771
+ 'vocabulary': 'Vocabulário',
772
+ 'structure': 'Estrutura',
773
+ 'cohesion': 'Coesão',
774
+ 'clarity': 'Clareza',
775
+ 'general': 'Geral'
776
+ },
777
+ 'ui_text': {
778
+ 'priority_intro': aqui que você deve concentrar seus esforços.",
779
+ 'detailed_recommendations': "Recomendações detalhadas",
780
+ 'save_button': "Salvar análise",
781
+ 'save_success': "Análise salva com sucesso",
782
+ 'save_error': "Erro ao salvar análise",
783
+ 'area_priority': "Área prioritária"
784
+ }
785
+ }
786
+ else: # Español por defecto
787
+ return {
788
+ 'vocabulary': ["Intenta enriquecer tu vocabulario"],
789
+ 'structure': ["Trabaja en la estructura de tus oraciones"],
790
+ 'cohesion': ["Mejora la conexión entre tus ideas"],
791
+ 'clarity': ["Busca expresar tus ideas con mayor claridad"],
792
+ 'specific': ["Adapta tu texto según su propósito"],
793
+ 'priority': {
794
+ 'area': 'general',
795
+ 'tips': ["Busca retroalimentación específica de un tutor o profesor"]
796
+ },
797
+ 'dimension_names': {
798
+ 'vocabulary': 'Vocabulario',
799
+ 'structure': 'Estructura',
800
+ 'cohesion': 'Cohesión',
801
+ 'clarity': 'Claridad',
802
+ 'general': 'General'
803
+ },
804
+ 'ui_text': {
805
+ 'priority_intro': "Esta es el área donde debes concentrar tus esfuerzos.",
806
+ 'detailed_recommendations': "Recomendaciones detalladas",
807
+ 'save_button': "Guardar análisis",
808
+ 'save_success': "Análisis guardado con éxito",
809
+ 'save_error': "Error al guardar el análisis",
810
+ 'area_priority': "Área prioritaria"
811
+ }
812
+ }
813
+
814
+
815
+ #########################################################################
816
+ #########################################################################
817
+ # Funciones de generación de gráficos
818
+ def generate_sentence_graphs(doc):
819
+ """Genera visualizaciones de estructura de oraciones"""
820
+ fig, ax = plt.subplots(figsize=(10, 6))
821
+ # Implementar visualización
822
+ plt.close()
823
+ return fig
824
+
825
+ def generate_word_connections(doc):
826
+ """Genera red de conexiones de palabras"""
827
+ fig, ax = plt.subplots(figsize=(10, 6))
828
+ # Implementar visualización
829
+ plt.close()
830
+ return fig
831
+
832
+ def generate_connection_paths(doc):
833
+ """Genera patrones de conexión"""
834
+ fig, ax = plt.subplots(figsize=(10, 6))
835
+ # Implementar visualización
836
+ plt.close()
837
+ return fig
838
+
839
+ def create_vocabulary_network(doc):
840
+ """
841
+ Genera el grafo de red de vocabulario.
842
+ """
843
+ G = nx.Graph()
844
+
845
+ # Crear nodos para palabras significativas
846
+ words = [token.text.lower() for token in doc if token.is_alpha and not token.is_stop]
847
+ word_freq = Counter(words)
848
+
849
+ # Añadir nodos con tamaño basado en frecuencia
850
+ for word, freq in word_freq.items():
851
+ G.add_node(word, size=freq)
852
+
853
+ # Crear conexiones basadas en co-ocurrencia
854
+ window_size = 5
855
+ for i in range(len(words) - window_size):
856
+ window = words[i:i+window_size]
857
+ for w1, w2 in combinations(set(window), 2):
858
+ if G.has_edge(w1, w2):
859
+ G[w1][w2]['weight'] += 1
860
+ else:
861
+ G.add_edge(w1, w2, weight=1)
862
+
863
+ # Crear visualización
864
+ fig, ax = plt.subplots(figsize=(12, 8))
865
+ pos = nx.spring_layout(G)
866
+
867
+ # Dibujar nodos
868
+ nx.draw_networkx_nodes(G, pos,
869
+ node_size=[G.nodes[node]['size']*100 for node in G.nodes],
870
+ node_color='lightblue',
871
+ alpha=0.7)
872
+
873
+ # Dibujar conexiones
874
+ nx.draw_networkx_edges(G, pos,
875
+ width=[G[u][v]['weight']*0.5 for u,v in G.edges],
876
+ alpha=0.5)
877
+
878
+ # Añadir etiquetas
879
+ nx.draw_networkx_labels(G, pos)
880
+
881
+ plt.title("Red de Vocabulario")
882
+ plt.axis('off')
883
+ return fig
884
+
885
+ def create_syntax_complexity_graph(doc):
886
+ """
887
+ Genera el diagrama de arco de complejidad sintáctica.
888
+ Muestra la estructura de dependencias con colores basados en la complejidad.
889
+ """
890
+ try:
891
+ # Preparar datos para la visualización
892
+ sentences = list(doc.sents)
893
+ if not sentences:
894
+ return None
895
+
896
+ # Crear figura para el gráfico
897
+ fig, ax = plt.subplots(figsize=(12, len(sentences) * 2))
898
+
899
+ # Colores para diferentes niveles de profundidad
900
+ depth_colors = plt.cm.viridis(np.linspace(0, 1, 6))
901
+
902
+ y_offset = 0
903
+ max_x = 0
904
+
905
+ for sent in sentences:
906
+ words = [token.text for token in sent]
907
+ x_positions = range(len(words))
908
+ max_x = max(max_x, len(words))
909
+
910
+ # Dibujar palabras
911
+ plt.plot(x_positions, [y_offset] * len(words), 'k-', alpha=0.2)
912
+ plt.scatter(x_positions, [y_offset] * len(words), alpha=0)
913
+
914
+ # Añadir texto
915
+ for i, word in enumerate(words):
916
+ plt.annotate(word, (i, y_offset), xytext=(0, -10),
917
+ textcoords='offset points', ha='center')
918
+
919
+ # Dibujar arcos de dependencia
920
+ for token in sent:
921
+ if token.dep_ != "ROOT":
922
+ # Calcular profundidad de dependencia
923
+ depth = 0
924
+ current = token
925
+ while current.head != current:
926
+ depth += 1
927
+ current = current.head
928
+
929
+ # Determinar posiciones para el arco
930
+ start = token.i - sent[0].i
931
+ end = token.head.i - sent[0].i
932
+
933
+ # Altura del arco basada en la distancia entre palabras
934
+ height = 0.5 * abs(end - start)
935
+
936
+ # Color basado en la profundidad
937
+ color = depth_colors[min(depth, len(depth_colors)-1)]
938
+
939
+ # Crear arco
940
+ arc = patches.Arc((min(start, end) + abs(end - start)/2, y_offset),
941
+ width=abs(end - start),
942
+ height=height,
943
+ angle=0,
944
+ theta1=0,
945
+ theta2=180,
946
+ color=color,
947
+ alpha=0.6)
948
+ ax.add_patch(arc)
949
+
950
+ y_offset -= 2
951
+
952
+ # Configurar el gráfico
953
+ plt.xlim(-1, max_x)
954
+ plt.ylim(y_offset - 1, 1)
955
+ plt.axis('off')
956
+ plt.title("Complejidad Sintáctica")
957
+
958
+ return fig
959
+
960
+ except Exception as e:
961
+ logger.error(f"Error en create_syntax_complexity_graph: {str(e)}")
962
+ return None
963
+
964
+
965
+ def create_cohesion_heatmap(doc):
966
+ """Genera un mapa de calor que muestra la cohesión entre párrafos/oraciones."""
967
+ try:
968
+ sentences = list(doc.sents)
969
+ n_sentences = len(sentences)
970
+
971
+ if n_sentences < 2:
972
+ return None
973
+
974
+ similarity_matrix = np.zeros((n_sentences, n_sentences))
975
+
976
+ for i in range(n_sentences):
977
+ for j in range(n_sentences):
978
+ sent1_lemmas = {token.lemma_ for token in sentences[i]
979
+ if token.is_alpha and not token.is_stop}
980
+ sent2_lemmas = {token.lemma_ for token in sentences[j]
981
+ if token.is_alpha and not token.is_stop}
982
+
983
+ if sent1_lemmas and sent2_lemmas:
984
+ intersection = len(sent1_lemmas & sent2_lemmas) # Corregido aquí
985
+ union = len(sent1_lemmas | sent2_lemmas) # Y aquí
986
+ similarity_matrix[i, j] = intersection / union if union > 0 else 0
987
+
988
+ # Crear visualización
989
+ fig, ax = plt.subplots(figsize=(10, 8))
990
+
991
+ sns.heatmap(similarity_matrix,
992
+ cmap='YlOrRd',
993
+ square=True,
994
+ xticklabels=False,
995
+ yticklabels=False,
996
+ cbar_kws={'label': 'Cohesión'},
997
+ ax=ax)
998
+
999
+ plt.title("Mapa de Cohesión Textual")
1000
+ plt.xlabel("Oraciones")
1001
+ plt.ylabel("Oraciones")
1002
+
1003
+ plt.tight_layout()
1004
+ return fig
1005
+
1006
+ except Exception as e:
1007
+ logger.error(f"Error en create_cohesion_heatmap: {str(e)}")
1008
+ return None
modules/studentact/current_situation_interface--FAIL.py CHANGED
@@ -1,608 +1,608 @@
1
- # modules/studentact/current_situation_interface.py
2
-
3
- import streamlit as st
4
- import logging
5
- from ..utils.widget_utils import generate_unique_key
6
- import matplotlib.pyplot as plt
7
- import numpy as np
8
-
9
- from ..database.current_situation_mongo_db import store_current_situation_result
10
-
11
- from ..database.writing_progress_mongo_db import (
12
- store_writing_baseline,
13
- store_writing_progress,
14
- get_writing_baseline,
15
- get_writing_progress,
16
- get_latest_writing_metrics
17
- )
18
-
19
- from .current_situation_analysis import (
20
- analyze_text_dimensions,
21
- analyze_clarity,
22
- analyze_vocabulary_diversity,
23
- analyze_cohesion,
24
- analyze_structure,
25
- get_dependency_depths,
26
- normalize_score,
27
- generate_sentence_graphs,
28
- generate_word_connections,
29
- generate_connection_paths,
30
- create_vocabulary_network,
31
- create_syntax_complexity_graph,
32
- create_cohesion_heatmap
33
- )
34
-
35
- # Configuración del estilo de matplotlib para el gráfico de radar
36
- plt.rcParams['font.family'] = 'sans-serif'
37
- plt.rcParams['axes.grid'] = True
38
- plt.rcParams['axes.spines.top'] = False
39
- plt.rcParams['axes.spines.right'] = False
40
-
41
- logger = logging.getLogger(__name__)
42
- ####################################
43
-
44
- TEXT_TYPES = {
45
- 'academic_article': {
46
- 'name': 'Artículo Académico',
47
- 'thresholds': {
48
- 'vocabulary': {'min': 0.70, 'target': 0.85},
49
- 'structure': {'min': 0.75, 'target': 0.90},
50
- 'cohesion': {'min': 0.65, 'target': 0.80},
51
- 'clarity': {'min': 0.70, 'target': 0.85}
52
- }
53
- },
54
- 'student_essay': {
55
- 'name': 'Trabajo Universitario',
56
- 'thresholds': {
57
- 'vocabulary': {'min': 0.60, 'target': 0.75},
58
- 'structure': {'min': 0.65, 'target': 0.80},
59
- 'cohesion': {'min': 0.55, 'target': 0.70},
60
- 'clarity': {'min': 0.60, 'target': 0.75}
61
- }
62
- },
63
- 'general_communication': {
64
- 'name': 'Comunicación General',
65
- 'thresholds': {
66
- 'vocabulary': {'min': 0.50, 'target': 0.65},
67
- 'structure': {'min': 0.55, 'target': 0.70},
68
- 'cohesion': {'min': 0.45, 'target': 0.60},
69
- 'clarity': {'min': 0.50, 'target': 0.65}
70
- }
71
- }
72
- }
73
- ####################################
74
-
75
- ANALYSIS_DIMENSION_MAPPING = {
76
- 'morphosyntactic': {
77
- 'primary': ['vocabulary', 'clarity'],
78
- 'secondary': ['structure'],
79
- 'tools': ['arc_diagrams', 'word_repetition']
80
- },
81
- 'semantic': {
82
- 'primary': ['cohesion', 'structure'],
83
- 'secondary': ['vocabulary'],
84
- 'tools': ['concept_graphs', 'semantic_networks']
85
- },
86
- 'discourse': {
87
- 'primary': ['cohesion', 'structure'],
88
- 'secondary': ['clarity'],
89
- 'tools': ['comparative_analysis']
90
- }
91
- }
92
-
93
- ##############################################################################
94
- # FUNCIÓN PRINCIPAL
95
- ##############################################################################
96
- def display_current_situation_interface(lang_code, nlp_models, t):
97
- """
98
- TAB:
99
- - Expander con radio para tipo de texto
100
- Contenedor-1 con expanders:
101
- - Expander "Métricas de la línea base"
102
- - Expander "Métricas de la iteración"
103
- Contenedor-2 (2 columnas):
104
- - Col1: Texto base
105
- - Col2: Texto iteración
106
- Al final, Recomendaciones en un expander (una sola “fila”).
107
- """
108
-
109
- # --- Inicializar session_state ---
110
- if 'base_text' not in st.session_state:
111
- st.session_state.base_text = ""
112
- if 'iter_text' not in st.session_state:
113
- st.session_state.iter_text = ""
114
- if 'base_metrics' not in st.session_state:
115
- st.session_state.base_metrics = {}
116
- if 'iter_metrics' not in st.session_state:
117
- st.session_state.iter_metrics = {}
118
- if 'show_base' not in st.session_state:
119
- st.session_state.show_base = False
120
- if 'show_iter' not in st.session_state:
121
- st.session_state.show_iter = False
122
-
123
- # Creamos un tab
124
- tabs = st.tabs(["Análisis de Texto"])
125
- with tabs[0]:
126
- # [1] Expander con radio para seleccionar tipo de texto
127
- with st.expander("Selecciona el tipo de texto", expanded=True):
128
- text_type = st.radio(
129
- "¿Qué tipo de texto quieres analizar?",
130
- options=list(TEXT_TYPES.keys()),
131
- format_func=lambda x: TEXT_TYPES[x]['name'],
132
- index=0
133
- )
134
- st.session_state.current_text_type = text_type
135
-
136
- st.markdown("---")
137
-
138
- # ---------------------------------------------------------------------
139
- # CONTENEDOR-1: Expanders para métricas base e iteración
140
- # ---------------------------------------------------------------------
141
- with st.container():
142
- # --- Expander para la línea base ---
143
- with st.expander("Métricas de la línea base", expanded=False):
144
- if st.session_state.show_base and st.session_state.base_metrics:
145
- # Mostramos los valores reales
146
- display_metrics_in_one_row(st.session_state.base_metrics, text_type)
147
- else:
148
- # Mostramos la maqueta vacía
149
- display_empty_metrics_row()
150
-
151
- # --- Expander para la iteración ---
152
- with st.expander("Métricas de la iteración", expanded=False):
153
- if st.session_state.show_iter and st.session_state.iter_metrics:
154
- display_metrics_in_one_row(st.session_state.iter_metrics, text_type)
155
- else:
156
- display_empty_metrics_row()
157
-
158
- st.markdown("---")
159
-
160
- # ---------------------------------------------------------------------
161
- # CONTENEDOR-2: 2 columnas (texto base | texto iteración)
162
- # ---------------------------------------------------------------------
163
- with st.container():
164
- col_left, col_right = st.columns(2)
165
-
166
- # Columna izquierda: Texto base
167
- with col_left:
168
- st.markdown("**Texto base**")
169
- text_base = st.text_area(
170
- label="",
171
- value=st.session_state.base_text,
172
- key="text_base_area",
173
- placeholder="Pega aquí tu texto base",
174
- )
175
- if st.button("Analizar Base"):
176
- with st.spinner("Analizando texto base..."):
177
- doc = nlp_models[lang_code](text_base)
178
- metrics = analyze_text_dimensions(doc)
179
-
180
- st.session_state.base_text = text_base
181
- st.session_state.base_metrics = metrics
182
- st.session_state.show_base = True
183
- # Al analizar base, reiniciamos la iteración
184
- st.session_state.show_iter = False
185
-
186
- # Columna derecha: Texto iteración
187
- with col_right:
188
- st.markdown("**Texto de iteración**")
189
- text_iter = st.text_area(
190
- label="",
191
- value=st.session_state.iter_text,
192
- key="text_iter_area",
193
- placeholder="Edita y mejora tu texto...",
194
- disabled=not st.session_state.show_base
195
- )
196
- if st.button("Analizar Iteración", disabled=not st.session_state.show_base):
197
- with st.spinner("Analizando iteración..."):
198
- doc = nlp_models[lang_code](text_iter)
199
- metrics = analyze_text_dimensions(doc)
200
-
201
- st.session_state.iter_text = text_iter
202
- st.session_state.iter_metrics = metrics
203
- st.session_state.show_iter = True
204
-
205
- # ---------------------------------------------------------------------
206
- # Recomendaciones al final en un expander (una sola “fila”)
207
- # ---------------------------------------------------------------------
208
- if st.session_state.show_iter:
209
- with st.expander("Recomendaciones", expanded=False):
210
- reco_list = []
211
- for dimension, values in st.session_state.iter_metrics.items():
212
- score = values['normalized_score']
213
- target = TEXT_TYPES[text_type]['thresholds'][dimension]['target']
214
- if score < target:
215
- # Aquí, en lugar de get_dimension_suggestions, unificamos con:
216
- suggestions = suggest_improvement_tools_list(dimension)
217
- reco_list.extend(suggestions)
218
-
219
- if reco_list:
220
- # Todas en una sola línea
221
- st.write(" | ".join(reco_list))
222
- else:
223
- st.info("¡No hay recomendaciones! Todas las métricas superan la meta.")
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
- #Funciones de visualización ##################################
232
- ############################################################
233
- # Funciones de visualización para las métricas
234
- ############################################################
235
-
236
- def display_metrics_in_one_row(metrics, text_type):
237
- """
238
- Muestra las cuatro dimensiones (Vocabulario, Estructura, Cohesión, Claridad)
239
- en una sola línea, usando 4 columnas con ancho uniforme.
240
- """
241
- thresholds = TEXT_TYPES[text_type]['thresholds']
242
- dimensions = ["vocabulary", "structure", "cohesion", "clarity"]
243
-
244
- col1, col2, col3, col4 = st.columns([1,1,1,1])
245
- cols = [col1, col2, col3, col4]
246
-
247
- for dim, col in zip(dimensions, cols):
248
- score = metrics[dim]['normalized_score']
249
- target = thresholds[dim]['target']
250
- min_val = thresholds[dim]['min']
251
-
252
- if score < min_val:
253
- status = "⚠️ Por mejorar"
254
- color = "inverse"
255
- elif score < target:
256
- status = "📈 Aceptable"
257
- color = "off"
258
- else:
259
- status = "✅ Óptimo"
260
- color = "normal"
261
-
262
- with col:
263
- col.metric(
264
- label=dim.capitalize(),
265
- value=f"{score:.2f}",
266
- delta=f"{status} (Meta: {target:.2f})",
267
- delta_color=color,
268
- border=True
269
- )
270
-
271
-
272
- # -------------------------------------------------------------------------
273
- # Función que muestra una fila de 4 columnas “vacías”
274
- # -------------------------------------------------------------------------
275
- def display_empty_metrics_row():
276
- """
277
- Muestra una fila de 4 columnas vacías (Vocabulario, Estructura, Cohesión, Claridad).
278
- Cada columna se dibuja con st.metric en blanco (“-”).
279
- """
280
- empty_cols = st.columns([1,1,1,1])
281
- labels = ["Vocabulario", "Estructura", "Cohesión", "Claridad"]
282
-
283
- for col, lbl in zip(empty_cols, labels):
284
- with col:
285
- col.metric(
286
- label=lbl,
287
- value="-",
288
- delta="",
289
- border=True
290
- )
291
-
292
-
293
-
294
- ####################################################################
295
-
296
- def display_metrics_analysis(metrics, text_type=None):
297
- """
298
- Muestra los resultados del análisis: métricas verticalmente y gráfico radar.
299
- """
300
- try:
301
- # Usar valor por defecto si no se especifica tipo
302
- text_type = text_type or 'student_essay'
303
-
304
- # Obtener umbrales según el tipo de texto
305
- thresholds = TEXT_TYPES[text_type]['thresholds']
306
-
307
- # Crear dos columnas para las métricas y el gráfico
308
- metrics_col, graph_col = st.columns([1, 1.5])
309
-
310
- # Columna de métricas
311
- with metrics_col:
312
- metrics_config = [
313
- {
314
- 'label': "Vocabulario",
315
- 'key': 'vocabulary',
316
- 'value': metrics['vocabulary']['normalized_score'],
317
- 'help': "Riqueza y variedad del vocabulario",
318
- 'thresholds': thresholds['vocabulary']
319
- },
320
- {
321
- 'label': "Estructura",
322
- 'key': 'structure',
323
- 'value': metrics['structure']['normalized_score'],
324
- 'help': "Organización y complejidad de oraciones",
325
- 'thresholds': thresholds['structure']
326
- },
327
- {
328
- 'label': "Cohesión",
329
- 'key': 'cohesion',
330
- 'value': metrics['cohesion']['normalized_score'],
331
- 'help': "Conexión y fluidez entre ideas",
332
- 'thresholds': thresholds['cohesion']
333
- },
334
- {
335
- 'label': "Claridad",
336
- 'key': 'clarity',
337
- 'value': metrics['clarity']['normalized_score'],
338
- 'help': "Facilidad de comprensión del texto",
339
- 'thresholds': thresholds['clarity']
340
- }
341
- ]
342
-
343
- # Mostrar métricas
344
- for metric in metrics_config:
345
- value = metric['value']
346
- if value < metric['thresholds']['min']:
347
- status = "⚠️ Por mejorar"
348
- color = "inverse"
349
- elif value < metric['thresholds']['target']:
350
- status = "📈 Aceptable"
351
- color = "off"
352
- else:
353
- status = "✅ Óptimo"
354
- color = "normal"
355
-
356
- st.metric(
357
- metric['label'],
358
- f"{value:.2f}",
359
- f"{status} (Meta: {metric['thresholds']['target']:.2f})",
360
- delta_color=color,
361
- help=metric['help']
362
- )
363
- st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
364
-
365
- except Exception as e:
366
- logger.error(f"Error mostrando resultados: {str(e)}")
367
- st.error("Error al mostrar los resultados")
368
-
369
- def display_comparison_results(baseline_metrics, current_metrics):
370
- """Muestra comparación entre línea base y métricas actuales"""
371
-
372
- # Crear columnas para métricas y gráfico
373
- metrics_col, graph_col = st.columns([1, 1.5])
374
-
375
- with metrics_col:
376
- for dimension in ['vocabulary', 'structure', 'cohesion', 'clarity']:
377
- baseline = baseline_metrics[dimension]['normalized_score']
378
- current = current_metrics[dimension]['normalized_score']
379
- delta = current - baseline
380
-
381
- st.metric(
382
- dimension.title(),
383
- f"{current:.2f}",
384
- f"{delta:+.2f}",
385
- delta_color="normal" if delta >= 0 else "inverse"
386
- )
387
-
388
- # Sugerir herramientas de mejora
389
- if delta < 0:
390
- suggest_improvement_tools(dimension)
391
-
392
- with graph_col:
393
- display_radar_chart_comparison(
394
- baseline_metrics,
395
- current_metrics
396
- )
397
-
398
- def display_metrics_and_suggestions(metrics, text_type, title, show_suggestions=False):
399
- """
400
- Muestra métricas y opcionalmente sugerencias de mejora.
401
- Args:
402
- metrics: Diccionario con las métricas analizadas
403
- text_type: Tipo de texto seleccionado
404
- title: Título para las métricas ("Base" o "Iteración")
405
- show_suggestions: Booleano para mostrar sugerencias
406
- """
407
- try:
408
- thresholds = TEXT_TYPES[text_type]['thresholds']
409
-
410
- st.markdown(f"### Métricas {title}")
411
-
412
- for dimension, values in metrics.items():
413
- score = values['normalized_score']
414
- target = thresholds[dimension]['target']
415
- min_val = thresholds[dimension]['min']
416
-
417
- # Determinar estado y color
418
- if score < min_val:
419
- status = "⚠️ Por mejorar"
420
- color = "inverse"
421
- elif score < target:
422
- status = "📈 Aceptable"
423
- color = "off"
424
- else:
425
- status = "✅ Óptimo"
426
- color = "normal"
427
-
428
- # Mostrar métrica
429
- st.metric(
430
- dimension.title(),
431
- f"{score:.2f}",
432
- f"{status} (Meta: {target:.2f})",
433
- delta_color=color,
434
- help=f"Meta: {target:.2f}, Mínimo: {min_val:.2f}"
435
- )
436
-
437
- # Mostrar sugerencias si es necesario
438
- if show_suggestions and score < target:
439
- suggest_improvement_tools(dimension)
440
-
441
- # Agregar espacio entre métricas
442
- st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
443
-
444
- except Exception as e:
445
- logger.error(f"Error mostrando métricas: {str(e)}")
446
- st.error("Error al mostrar métricas")
447
-
448
- def display_radar_chart(metrics_config, thresholds, baseline_metrics=None):
449
- """
450
- Muestra el gráfico radar con los resultados.
451
- Args:
452
- metrics_config: Configuración actual de métricas
453
- thresholds: Umbrales para las métricas
454
- baseline_metrics: Métricas de línea base (opcional)
455
- """
456
- try:
457
- # Preparar datos para el gráfico
458
- categories = [m['label'] for m in metrics_config]
459
- values_current = [m['value'] for m in metrics_config]
460
- min_values = [m['thresholds']['min'] for m in metrics_config]
461
- target_values = [m['thresholds']['target'] for m in metrics_config]
462
-
463
- # Crear y configurar gráfico
464
- fig = plt.figure(figsize=(8, 8))
465
- ax = fig.add_subplot(111, projection='polar')
466
-
467
- # Configurar radar
468
- angles = [n / float(len(categories)) * 2 * np.pi for n in range(len(categories))]
469
- angles += angles[:1]
470
- values_current += values_current[:1]
471
- min_values += min_values[:1]
472
- target_values += target_values[:1]
473
-
474
- # Configurar ejes
475
- ax.set_xticks(angles[:-1])
476
- ax.set_xticklabels(categories, fontsize=10)
477
- circle_ticks = np.arange(0, 1.1, 0.2)
478
- ax.set_yticks(circle_ticks)
479
- ax.set_yticklabels([f'{tick:.1f}' for tick in circle_ticks], fontsize=8)
480
- ax.set_ylim(0, 1)
481
-
482
- # Dibujar áreas de umbrales
483
- ax.plot(angles, min_values, '#e74c3c', linestyle='--', linewidth=1,
484
- label='Mínimo', alpha=0.5)
485
- ax.plot(angles, target_values, '#2ecc71', linestyle='--', linewidth=1,
486
- label='Meta', alpha=0.5)
487
- ax.fill_between(angles, target_values, [1]*len(angles),
488
- color='#2ecc71', alpha=0.1)
489
- ax.fill_between(angles, [0]*len(angles), min_values,
490
- color='#e74c3c', alpha=0.1)
491
-
492
- # Si hay línea base, dibujarla primero
493
- if baseline_metrics is not None:
494
- values_baseline = [baseline_metrics[m['key']]['normalized_score']
495
- for m in metrics_config]
496
- values_baseline += values_baseline[:1]
497
- ax.plot(angles, values_baseline, '#888888', linewidth=2,
498
- label='Línea base', linestyle='--')
499
- ax.fill(angles, values_baseline, '#888888', alpha=0.1)
500
-
501
- # Dibujar valores actuales
502
- label = 'Actual' if baseline_metrics else 'Tu escritura'
503
- color = '#3498db' if baseline_metrics else '#3498db'
504
-
505
- ax.plot(angles, values_current, color, linewidth=2, label=label)
506
- ax.fill(angles, values_current, color, alpha=0.2)
507
-
508
- # Ajustar leyenda
509
- legend_handles = []
510
- if baseline_metrics:
511
- legend_handles.extend([
512
- plt.Line2D([], [], color='#888888', linestyle='--',
513
- label='Línea base'),
514
- plt.Line2D([], [], color='#3498db', label='Actual')
515
- ])
516
- else:
517
- legend_handles.extend([
518
- plt.Line2D([], [], color='#3498db', label='Tu escritura')
519
- ])
520
-
521
- legend_handles.extend([
522
- plt.Line2D([], [], color='#e74c3c', linestyle='--', label='Mínimo'),
523
- plt.Line2D([], [], color='#2ecc71', linestyle='--', label='Meta')
524
- ])
525
-
526
- ax.legend(
527
- handles=legend_handles,
528
- loc='upper right',
529
- bbox_to_anchor=(1.3, 1.1),
530
- fontsize=10,
531
- frameon=True,
532
- facecolor='white',
533
- edgecolor='none',
534
- shadow=True
535
- )
536
-
537
- plt.tight_layout()
538
- st.pyplot(fig)
539
- plt.close()
540
-
541
- except Exception as e:
542
- logger.error(f"Error mostrando gráfico radar: {str(e)}")
543
- st.error("Error al mostrar el gráfico")
544
-
545
- #Funciones auxiliares ##################################
546
-
547
-
548
- ############################################################
549
- # Unificamos la lógica de sugerencias en una función
550
- ############################################################
551
- def suggest_improvement_tools_list(dimension):
552
- """
553
- Retorna en forma de lista las herramientas sugeridas
554
- basadas en 'ANALYSIS_DIMENSION_MAPPING'.
555
- """
556
- suggestions = []
557
- for analysis, mapping in ANALYSIS_DIMENSION_MAPPING.items():
558
- # Verificamos si la dimensión está en primary o secondary
559
- if dimension in mapping['primary'] or dimension in mapping['secondary']:
560
- suggestions.extend(mapping['tools'])
561
- # Si no hay nada, al menos retornamos un placeholder
562
- return suggestions if suggestions else ["Sin sugerencias específicas."]
563
-
564
-
565
- def prepare_metrics_config(metrics, text_type='student_essay'):
566
- """
567
- Prepara la configuración de métricas en el mismo formato que display_results.
568
- Args:
569
- metrics: Diccionario con las métricas analizadas
570
- text_type: Tipo de texto para los umbrales
571
- Returns:
572
- list: Lista de configuraciones de métricas
573
- """
574
- # Obtener umbrales según el tipo de texto
575
- thresholds = TEXT_TYPES[text_type]['thresholds']
576
-
577
- # Usar la misma estructura que en display_results
578
- return [
579
- {
580
- 'label': "Vocabulario",
581
- 'key': 'vocabulary',
582
- 'value': metrics['vocabulary']['normalized_score'],
583
- 'help': "Riqueza y variedad del vocabulario",
584
- 'thresholds': thresholds['vocabulary']
585
- },
586
- {
587
- 'label': "Estructura",
588
- 'key': 'structure',
589
- 'value': metrics['structure']['normalized_score'],
590
- 'help': "Organización y complejidad de oraciones",
591
- 'thresholds': thresholds['structure']
592
- },
593
- {
594
- 'label': "Cohesión",
595
- 'key': 'cohesion',
596
- 'value': metrics['cohesion']['normalized_score'],
597
- 'help': "Conexión y fluidez entre ideas",
598
- 'thresholds': thresholds['cohesion']
599
- },
600
- {
601
- 'label': "Claridad",
602
- 'key': 'clarity',
603
- 'value': metrics['clarity']['normalized_score'],
604
- 'help': "Facilidad de comprensión del texto",
605
- 'thresholds': thresholds['clarity']
606
- }
607
- ]
608
-
 
1
+ # modules/studentact/current_situation_interface.py
2
+
3
+ import streamlit as st
4
+ import logging
5
+ from ..utils.widget_utils import generate_unique_key
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+
9
+ from ..database.current_situation_mongo_db import store_current_situation_result
10
+
11
+ from ..database.writing_progress_mongo_db import (
12
+ store_writing_baseline,
13
+ store_writing_progress,
14
+ get_writing_baseline,
15
+ get_writing_progress,
16
+ get_latest_writing_metrics
17
+ )
18
+
19
+ from .current_situation_analysis import (
20
+ analyze_text_dimensions,
21
+ analyze_clarity,
22
+ analyze_vocabulary_diversity,
23
+ analyze_cohesion,
24
+ analyze_structure,
25
+ get_dependency_depths,
26
+ normalize_score,
27
+ generate_sentence_graphs,
28
+ generate_word_connections,
29
+ generate_connection_paths,
30
+ create_vocabulary_network,
31
+ create_syntax_complexity_graph,
32
+ create_cohesion_heatmap
33
+ )
34
+
35
+ # Configuración del estilo de matplotlib para el gráfico de radar
36
+ plt.rcParams['font.family'] = 'sans-serif'
37
+ plt.rcParams['axes.grid'] = True
38
+ plt.rcParams['axes.spines.top'] = False
39
+ plt.rcParams['axes.spines.right'] = False
40
+
41
+ logger = logging.getLogger(__name__)
42
+ ####################################
43
+
44
+ TEXT_TYPES = {
45
+ 'academic_article': {
46
+ 'name': 'Artículo Académico',
47
+ 'thresholds': {
48
+ 'vocabulary': {'min': 0.70, 'target': 0.85},
49
+ 'structure': {'min': 0.75, 'target': 0.90},
50
+ 'cohesion': {'min': 0.65, 'target': 0.80},
51
+ 'clarity': {'min': 0.70, 'target': 0.85}
52
+ }
53
+ },
54
+ 'student_essay': {
55
+ 'name': 'Trabajo Universitario',
56
+ 'thresholds': {
57
+ 'vocabulary': {'min': 0.60, 'target': 0.75},
58
+ 'structure': {'min': 0.65, 'target': 0.80},
59
+ 'cohesion': {'min': 0.55, 'target': 0.70},
60
+ 'clarity': {'min': 0.60, 'target': 0.75}
61
+ }
62
+ },
63
+ 'general_communication': {
64
+ 'name': 'Comunicación General',
65
+ 'thresholds': {
66
+ 'vocabulary': {'min': 0.50, 'target': 0.65},
67
+ 'structure': {'min': 0.55, 'target': 0.70},
68
+ 'cohesion': {'min': 0.45, 'target': 0.60},
69
+ 'clarity': {'min': 0.50, 'target': 0.65}
70
+ }
71
+ }
72
+ }
73
+ ####################################
74
+
75
+ ANALYSIS_DIMENSION_MAPPING = {
76
+ 'morphosyntactic': {
77
+ 'primary': ['vocabulary', 'clarity'],
78
+ 'secondary': ['structure'],
79
+ 'tools': ['arc_diagrams', 'word_repetition']
80
+ },
81
+ 'semantic': {
82
+ 'primary': ['cohesion', 'structure'],
83
+ 'secondary': ['vocabulary'],
84
+ 'tools': ['concept_graphs', 'semantic_networks']
85
+ },
86
+ 'discourse': {
87
+ 'primary': ['cohesion', 'structure'],
88
+ 'secondary': ['clarity'],
89
+ 'tools': ['comparative_analysis']
90
+ }
91
+ }
92
+
93
+ ##############################################################################
94
+ # FUNCIÓN PRINCIPAL
95
+ ##############################################################################
96
+ def display_current_situation_interface(lang_code, nlp_models, t):
97
+ """
98
+ TAB:
99
+ - Expander con radio para tipo de texto
100
+ Contenedor-1 con expanders:
101
+ - Expander "Métricas de la línea base"
102
+ - Expander "Métricas de la iteración"
103
+ Contenedor-2 (2 columnas):
104
+ - Col1: Texto base
105
+ - Col2: Texto iteración
106
+ Al final, Recomendaciones en un expander (una sola “fila”).
107
+ """
108
+
109
+ # --- Inicializar session_state ---
110
+ if 'base_text' not in st.session_state:
111
+ st.session_state.base_text = ""
112
+ if 'iter_text' not in st.session_state:
113
+ st.session_state.iter_text = ""
114
+ if 'base_metrics' not in st.session_state:
115
+ st.session_state.base_metrics = {}
116
+ if 'iter_metrics' not in st.session_state:
117
+ st.session_state.iter_metrics = {}
118
+ if 'show_base' not in st.session_state:
119
+ st.session_state.show_base = False
120
+ if 'show_iter' not in st.session_state:
121
+ st.session_state.show_iter = False
122
+
123
+ # Creamos un tab
124
+ tabs = st.tabs(["Análisis de Texto"])
125
+ with tabs[0]:
126
+ # [1] Expander con radio para seleccionar tipo de texto
127
+ with st.expander("Selecciona el tipo de texto", expanded=True):
128
+ text_type = st.radio(
129
+ "¿Qué tipo de texto quieres analizar?",
130
+ options=list(TEXT_TYPES.keys()),
131
+ format_func=lambda x: TEXT_TYPES[x]['name'],
132
+ index=0
133
+ )
134
+ st.session_state.current_text_type = text_type
135
+
136
+ st.markdown("---")
137
+
138
+ # ---------------------------------------------------------------------
139
+ # CONTENEDOR-1: Expanders para métricas base e iteración
140
+ # ---------------------------------------------------------------------
141
+ with st.container():
142
+ # --- Expander para la línea base ---
143
+ with st.expander("Métricas de la línea base", expanded=False):
144
+ if st.session_state.show_base and st.session_state.base_metrics:
145
+ # Mostramos los valores reales
146
+ display_metrics_in_one_row(st.session_state.base_metrics, text_type)
147
+ else:
148
+ # Mostramos la maqueta vacía
149
+ display_empty_metrics_row()
150
+
151
+ # --- Expander para la iteración ---
152
+ with st.expander("Métricas de la iteración", expanded=False):
153
+ if st.session_state.show_iter and st.session_state.iter_metrics:
154
+ display_metrics_in_one_row(st.session_state.iter_metrics, text_type)
155
+ else:
156
+ display_empty_metrics_row()
157
+
158
+ st.markdown("---")
159
+
160
+ # ---------------------------------------------------------------------
161
+ # CONTENEDOR-2: 2 columnas (texto base | texto iteración)
162
+ # ---------------------------------------------------------------------
163
+ with st.container():
164
+ col_left, col_right = st.columns(2)
165
+
166
+ # Columna izquierda: Texto base
167
+ with col_left:
168
+ st.markdown("**Texto base**")
169
+ text_base = st.text_area(
170
+ label="",
171
+ value=st.session_state.base_text,
172
+ key="text_base_area",
173
+ placeholder="Pega aquí tu texto base",
174
+ )
175
+ if st.button("Analizar Base"):
176
+ with st.spinner("Analizando texto base..."):
177
+ doc = nlp_models[lang_code](text_base)
178
+ metrics = analyze_text_dimensions(doc)
179
+
180
+ st.session_state.base_text = text_base
181
+ st.session_state.base_metrics = metrics
182
+ st.session_state.show_base = True
183
+ # Al analizar base, reiniciamos la iteración
184
+ st.session_state.show_iter = False
185
+
186
+ # Columna derecha: Texto iteración
187
+ with col_right:
188
+ st.markdown("**Texto de iteración**")
189
+ text_iter = st.text_area(
190
+ label="",
191
+ value=st.session_state.iter_text,
192
+ key="text_iter_area",
193
+ placeholder="Edita y mejora tu texto...",
194
+ disabled=not st.session_state.show_base
195
+ )
196
+ if st.button("Analizar Iteración", disabled=not st.session_state.show_base):
197
+ with st.spinner("Analizando iteración..."):
198
+ doc = nlp_models[lang_code](text_iter)
199
+ metrics = analyze_text_dimensions(doc)
200
+
201
+ st.session_state.iter_text = text_iter
202
+ st.session_state.iter_metrics = metrics
203
+ st.session_state.show_iter = True
204
+
205
+ # ---------------------------------------------------------------------
206
+ # Recomendaciones al final en un expander (una sola “fila”)
207
+ # ---------------------------------------------------------------------
208
+ if st.session_state.show_iter:
209
+ with st.expander("Recomendaciones", expanded=False):
210
+ reco_list = []
211
+ for dimension, values in st.session_state.iter_metrics.items():
212
+ score = values['normalized_score']
213
+ target = TEXT_TYPES[text_type]['thresholds'][dimension]['target']
214
+ if score < target:
215
+ # Aquí, en lugar de get_dimension_suggestions, unificamos con:
216
+ suggestions = suggest_improvement_tools_list(dimension)
217
+ reco_list.extend(suggestions)
218
+
219
+ if reco_list:
220
+ # Todas en una sola línea
221
+ st.write(" | ".join(reco_list))
222
+ else:
223
+ st.info("¡No hay recomendaciones! Todas las métricas superan la meta.")
224
+
225
+
226
+
227
+
228
+
229
+
230
+
231
+ #Funciones de visualización ##################################
232
+ ############################################################
233
+ # Funciones de visualización para las métricas
234
+ ############################################################
235
+
236
+ def display_metrics_in_one_row(metrics, text_type):
237
+ """
238
+ Muestra las cuatro dimensiones (Vocabulario, Estructura, Cohesión, Claridad)
239
+ en una sola línea, usando 4 columnas con ancho uniforme.
240
+ """
241
+ thresholds = TEXT_TYPES[text_type]['thresholds']
242
+ dimensions = ["vocabulary", "structure", "cohesion", "clarity"]
243
+
244
+ col1, col2, col3, col4 = st.columns([1,1,1,1])
245
+ cols = [col1, col2, col3, col4]
246
+
247
+ for dim, col in zip(dimensions, cols):
248
+ score = metrics[dim]['normalized_score']
249
+ target = thresholds[dim]['target']
250
+ min_val = thresholds[dim]['min']
251
+
252
+ if score < min_val:
253
+ status = "⚠️ Por mejorar"
254
+ color = "inverse"
255
+ elif score < target:
256
+ status = "📈 Aceptable"
257
+ color = "off"
258
+ else:
259
+ status = "✅ Óptimo"
260
+ color = "normal"
261
+
262
+ with col:
263
+ col.metric(
264
+ label=dim.capitalize(),
265
+ value=f"{score:.2f}",
266
+ delta=f"{status} (Meta: {target:.2f})",
267
+ delta_color=color,
268
+ border=True
269
+ )
270
+
271
+
272
+ # -------------------------------------------------------------------------
273
+ # Función que muestra una fila de 4 columnas “vacías”
274
+ # -------------------------------------------------------------------------
275
+ def display_empty_metrics_row():
276
+ """
277
+ Muestra una fila de 4 columnas vacías (Vocabulario, Estructura, Cohesión, Claridad).
278
+ Cada columna se dibuja con st.metric en blanco (“-”).
279
+ """
280
+ empty_cols = st.columns([1,1,1,1])
281
+ labels = ["Vocabulario", "Estructura", "Cohesión", "Claridad"]
282
+
283
+ for col, lbl in zip(empty_cols, labels):
284
+ with col:
285
+ col.metric(
286
+ label=lbl,
287
+ value="-",
288
+ delta="",
289
+ border=True
290
+ )
291
+
292
+
293
+
294
+ ####################################################################
295
+
296
+ def display_metrics_analysis(metrics, text_type=None):
297
+ """
298
+ Muestra los resultados del análisis: métricas verticalmente y gráfico radar.
299
+ """
300
+ try:
301
+ # Usar valor por defecto si no se especifica tipo
302
+ text_type = text_type or 'student_essay'
303
+
304
+ # Obtener umbrales según el tipo de texto
305
+ thresholds = TEXT_TYPES[text_type]['thresholds']
306
+
307
+ # Crear dos columnas para las métricas y el gráfico
308
+ metrics_col, graph_col = st.columns([1, 1.5])
309
+
310
+ # Columna de métricas
311
+ with metrics_col:
312
+ metrics_config = [
313
+ {
314
+ 'label': "Vocabulario",
315
+ 'key': 'vocabulary',
316
+ 'value': metrics['vocabulary']['normalized_score'],
317
+ 'help': "Riqueza y variedad del vocabulario",
318
+ 'thresholds': thresholds['vocabulary']
319
+ },
320
+ {
321
+ 'label': "Estructura",
322
+ 'key': 'structure',
323
+ 'value': metrics['structure']['normalized_score'],
324
+ 'help': "Organización y complejidad de oraciones",
325
+ 'thresholds': thresholds['structure']
326
+ },
327
+ {
328
+ 'label': "Cohesión",
329
+ 'key': 'cohesion',
330
+ 'value': metrics['cohesion']['normalized_score'],
331
+ 'help': "Conexión y fluidez entre ideas",
332
+ 'thresholds': thresholds['cohesion']
333
+ },
334
+ {
335
+ 'label': "Claridad",
336
+ 'key': 'clarity',
337
+ 'value': metrics['clarity']['normalized_score'],
338
+ 'help': "Facilidad de comprensión del texto",
339
+ 'thresholds': thresholds['clarity']
340
+ }
341
+ ]
342
+
343
+ # Mostrar métricas
344
+ for metric in metrics_config:
345
+ value = metric['value']
346
+ if value < metric['thresholds']['min']:
347
+ status = "⚠️ Por mejorar"
348
+ color = "inverse"
349
+ elif value < metric['thresholds']['target']:
350
+ status = "📈 Aceptable"
351
+ color = "off"
352
+ else:
353
+ status = "✅ Óptimo"
354
+ color = "normal"
355
+
356
+ st.metric(
357
+ metric['label'],
358
+ f"{value:.2f}",
359
+ f"{status} (Meta: {metric['thresholds']['target']:.2f})",
360
+ delta_color=color,
361
+ help=metric['help']
362
+ )
363
+ st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
364
+
365
+ except Exception as e:
366
+ logger.error(f"Error mostrando resultados: {str(e)}")
367
+ st.error("Error al mostrar los resultados")
368
+
369
+ def display_comparison_results(baseline_metrics, current_metrics):
370
+ """Muestra comparación entre línea base y métricas actuales"""
371
+
372
+ # Crear columnas para métricas y gráfico
373
+ metrics_col, graph_col = st.columns([1, 1.5])
374
+
375
+ with metrics_col:
376
+ for dimension in ['vocabulary', 'structure', 'cohesion', 'clarity']:
377
+ baseline = baseline_metrics[dimension]['normalized_score']
378
+ current = current_metrics[dimension]['normalized_score']
379
+ delta = current - baseline
380
+
381
+ st.metric(
382
+ dimension.title(),
383
+ f"{current:.2f}",
384
+ f"{delta:+.2f}",
385
+ delta_color="normal" if delta >= 0 else "inverse"
386
+ )
387
+
388
+ # Sugerir herramientas de mejora
389
+ if delta < 0:
390
+ suggest_improvement_tools(dimension)
391
+
392
+ with graph_col:
393
+ display_radar_chart_comparison(
394
+ baseline_metrics,
395
+ current_metrics
396
+ )
397
+
398
+ def display_metrics_and_suggestions(metrics, text_type, title, show_suggestions=False):
399
+ """
400
+ Muestra métricas y opcionalmente sugerencias de mejora.
401
+ Args:
402
+ metrics: Diccionario con las métricas analizadas
403
+ text_type: Tipo de texto seleccionado
404
+ title: Título para las métricas ("Base" o "Iteración")
405
+ show_suggestions: Booleano para mostrar sugerencias
406
+ """
407
+ try:
408
+ thresholds = TEXT_TYPES[text_type]['thresholds']
409
+
410
+ st.markdown(f"### Métricas {title}")
411
+
412
+ for dimension, values in metrics.items():
413
+ score = values['normalized_score']
414
+ target = thresholds[dimension]['target']
415
+ min_val = thresholds[dimension]['min']
416
+
417
+ # Determinar estado y color
418
+ if score < min_val:
419
+ status = "⚠️ Por mejorar"
420
+ color = "inverse"
421
+ elif score < target:
422
+ status = "📈 Aceptable"
423
+ color = "off"
424
+ else:
425
+ status = "✅ Óptimo"
426
+ color = "normal"
427
+
428
+ # Mostrar métrica
429
+ st.metric(
430
+ dimension.title(),
431
+ f"{score:.2f}",
432
+ f"{status} (Meta: {target:.2f})",
433
+ delta_color=color,
434
+ help=f"Meta: {target:.2f}, Mínimo: {min_val:.2f}"
435
+ )
436
+
437
+ # Mostrar sugerencias si es necesario
438
+ if show_suggestions and score < target:
439
+ suggest_improvement_tools(dimension)
440
+
441
+ # Agregar espacio entre métricas
442
+ st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
443
+
444
+ except Exception as e:
445
+ logger.error(f"Error mostrando métricas: {str(e)}")
446
+ st.error("Error al mostrar métricas")
447
+
448
+ def display_radar_chart(metrics_config, thresholds, baseline_metrics=None):
449
+ """
450
+ Muestra el gráfico radar con los resultados.
451
+ Args:
452
+ metrics_config: Configuración actual de métricas
453
+ thresholds: Umbrales para las métricas
454
+ baseline_metrics: Métricas de línea base (opcional)
455
+ """
456
+ try:
457
+ # Preparar datos para el gráfico
458
+ categories = [m['label'] for m in metrics_config]
459
+ values_current = [m['value'] for m in metrics_config]
460
+ min_values = [m['thresholds']['min'] for m in metrics_config]
461
+ target_values = [m['thresholds']['target'] for m in metrics_config]
462
+
463
+ # Crear y configurar gráfico
464
+ fig = plt.figure(figsize=(8, 8))
465
+ ax = fig.add_subplot(111, projection='polar')
466
+
467
+ # Configurar radar
468
+ angles = [n / float(len(categories)) * 2 * np.pi for n in range(len(categories))]
469
+ angles += angles[:1]
470
+ values_current += values_current[:1]
471
+ min_values += min_values[:1]
472
+ target_values += target_values[:1]
473
+
474
+ # Configurar ejes
475
+ ax.set_xticks(angles[:-1])
476
+ ax.set_xticklabels(categories, fontsize=10)
477
+ circle_ticks = np.arange(0, 1.1, 0.2)
478
+ ax.set_yticks(circle_ticks)
479
+ ax.set_yticklabels([f'{tick:.1f}' for tick in circle_ticks], fontsize=8)
480
+ ax.set_ylim(0, 1)
481
+
482
+ # Dibujar áreas de umbrales
483
+ ax.plot(angles, min_values, '#e74c3c', linestyle='--', linewidth=1,
484
+ label='Mínimo', alpha=0.5)
485
+ ax.plot(angles, target_values, '#2ecc71', linestyle='--', linewidth=1,
486
+ label='Meta', alpha=0.5)
487
+ ax.fill_between(angles, target_values, [1]*len(angles),
488
+ color='#2ecc71', alpha=0.1)
489
+ ax.fill_between(angles, [0]*len(angles), min_values,
490
+ color='#e74c3c', alpha=0.1)
491
+
492
+ # Si hay línea base, dibujarla primero
493
+ if baseline_metrics is not None:
494
+ values_baseline = [baseline_metrics[m['key']]['normalized_score']
495
+ for m in metrics_config]
496
+ values_baseline += values_baseline[:1]
497
+ ax.plot(angles, values_baseline, '#888888', linewidth=2,
498
+ label='Línea base', linestyle='--')
499
+ ax.fill(angles, values_baseline, '#888888', alpha=0.1)
500
+
501
+ # Dibujar valores actuales
502
+ label = 'Actual' if baseline_metrics else 'Tu escritura'
503
+ color = '#3498db' if baseline_metrics else '#3498db'
504
+
505
+ ax.plot(angles, values_current, color, linewidth=2, label=label)
506
+ ax.fill(angles, values_current, color, alpha=0.2)
507
+
508
+ # Ajustar leyenda
509
+ legend_handles = []
510
+ if baseline_metrics:
511
+ legend_handles.extend([
512
+ plt.Line2D([], [], color='#888888', linestyle='--',
513
+ label='Línea base'),
514
+ plt.Line2D([], [], color='#3498db', label='Actual')
515
+ ])
516
+ else:
517
+ legend_handles.extend([
518
+ plt.Line2D([], [], color='#3498db', label='Tu escritura')
519
+ ])
520
+
521
+ legend_handles.extend([
522
+ plt.Line2D([], [], color='#e74c3c', linestyle='--', label='Mínimo'),
523
+ plt.Line2D([], [], color='#2ecc71', linestyle='--', label='Meta')
524
+ ])
525
+
526
+ ax.legend(
527
+ handles=legend_handles,
528
+ loc='upper right',
529
+ bbox_to_anchor=(1.3, 1.1),
530
+ fontsize=10,
531
+ frameon=True,
532
+ facecolor='white',
533
+ edgecolor='none',
534
+ shadow=True
535
+ )
536
+
537
+ plt.tight_layout()
538
+ st.pyplot(fig)
539
+ plt.close()
540
+
541
+ except Exception as e:
542
+ logger.error(f"Error mostrando gráfico radar: {str(e)}")
543
+ st.error("Error al mostrar el gráfico")
544
+
545
+ #Funciones auxiliares ##################################
546
+
547
+
548
+ ############################################################
549
+ # Unificamos la lógica de sugerencias en una función
550
+ ############################################################
551
+ def suggest_improvement_tools_list(dimension):
552
+ """
553
+ Retorna en forma de lista las herramientas sugeridas
554
+ basadas en 'ANALYSIS_DIMENSION_MAPPING'.
555
+ """
556
+ suggestions = []
557
+ for analysis, mapping in ANALYSIS_DIMENSION_MAPPING.items():
558
+ # Verificamos si la dimensión está en primary o secondary
559
+ if dimension in mapping['primary'] or dimension in mapping['secondary']:
560
+ suggestions.extend(mapping['tools'])
561
+ # Si no hay nada, al menos retornamos un placeholder
562
+ return suggestions if suggestions else ["Sin sugerencias específicas."]
563
+
564
+
565
+ def prepare_metrics_config(metrics, text_type='student_essay'):
566
+ """
567
+ Prepara la configuración de métricas en el mismo formato que display_results.
568
+ Args:
569
+ metrics: Diccionario con las métricas analizadas
570
+ text_type: Tipo de texto para los umbrales
571
+ Returns:
572
+ list: Lista de configuraciones de métricas
573
+ """
574
+ # Obtener umbrales según el tipo de texto
575
+ thresholds = TEXT_TYPES[text_type]['thresholds']
576
+
577
+ # Usar la misma estructura que en display_results
578
+ return [
579
+ {
580
+ 'label': "Vocabulario",
581
+ 'key': 'vocabulary',
582
+ 'value': metrics['vocabulary']['normalized_score'],
583
+ 'help': "Riqueza y variedad del vocabulario",
584
+ 'thresholds': thresholds['vocabulary']
585
+ },
586
+ {
587
+ 'label': "Estructura",
588
+ 'key': 'structure',
589
+ 'value': metrics['structure']['normalized_score'],
590
+ 'help': "Organización y complejidad de oraciones",
591
+ 'thresholds': thresholds['structure']
592
+ },
593
+ {
594
+ 'label': "Cohesión",
595
+ 'key': 'cohesion',
596
+ 'value': metrics['cohesion']['normalized_score'],
597
+ 'help': "Conexión y fluidez entre ideas",
598
+ 'thresholds': thresholds['cohesion']
599
+ },
600
+ {
601
+ 'label': "Claridad",
602
+ 'key': 'clarity',
603
+ 'value': metrics['clarity']['normalized_score'],
604
+ 'help': "Facilidad de comprensión del texto",
605
+ 'thresholds': thresholds['clarity']
606
+ }
607
+ ]
608
+
modules/studentact/current_situation_interface-v1.py CHANGED
@@ -1,272 +1,272 @@
1
- # modules/studentact/current_situation_interface.py
2
-
3
- import streamlit as st
4
- import logging
5
- from ..utils.widget_utils import generate_unique_key
6
- from .current_situation_analysis import (
7
- analyze_text_dimensions,
8
- analyze_clarity,
9
- analyze_reference_clarity,
10
- analyze_vocabulary_diversity,
11
- analyze_cohesion,
12
- analyze_structure,
13
- get_dependency_depths,
14
- normalize_score,
15
- generate_sentence_graphs,
16
- generate_word_connections,
17
- generate_connection_paths,
18
- create_vocabulary_network,
19
- create_syntax_complexity_graph,
20
- create_cohesion_heatmap,
21
- )
22
-
23
- logger = logging.getLogger(__name__)
24
- ####################################
25
- def display_current_situation_interface(lang_code, nlp_models, t):
26
- """
27
- Interfaz simplificada para el análisis inicial, enfocada en recomendaciones directas.
28
- """
29
- # Inicializar estados si no existen
30
- if 'text_input' not in st.session_state:
31
- st.session_state.text_input = ""
32
- if 'show_results' not in st.session_state:
33
- st.session_state.show_results = False
34
- if 'current_doc' not in st.session_state:
35
- st.session_state.current_doc = None
36
- if 'current_metrics' not in st.session_state:
37
- st.session_state.current_metrics = None
38
-
39
- st.markdown("## Análisis Inicial de Escritura")
40
-
41
- # Container principal con dos columnas
42
- with st.container():
43
- input_col, results_col = st.columns([1,2])
44
-
45
- with input_col:
46
- st.markdown("### Ingresa tu texto")
47
-
48
- # Función para manejar cambios en el texto
49
- def on_text_change():
50
- st.session_state.text_input = st.session_state.text_area
51
- st.session_state.show_results = False # Resetear resultados cuando el texto cambia
52
-
53
- # Text area con manejo de estado
54
- text_input = st.text_area(
55
- t.get('input_prompt', "Escribe o pega tu texto aquí:"),
56
- height=400,
57
- key="text_area",
58
- value=st.session_state.text_input,
59
- on_change=on_text_change,
60
- help="Este texto será analizado para darte recomendaciones personalizadas"
61
- )
62
-
63
- # Botón de análisis
64
- if st.button(
65
- t.get('analyze_button', "Analizar mi escritura"),
66
- type="primary",
67
- disabled=not text_input.strip(),
68
- use_container_width=True,
69
- ):
70
- try:
71
- with st.spinner(t.get('processing', "Analizando...")):
72
- # Procesar texto y obtener métricas
73
- doc = nlp_models[lang_code](text_input)
74
- metrics = analyze_text_dimensions(doc)
75
-
76
- # Actualizar estado con nuevos resultados
77
- st.session_state.current_doc = doc
78
- st.session_state.current_metrics = metrics
79
- st.session_state.show_results = True
80
-
81
- # Mantener el texto en el estado
82
- st.session_state.text_input = text_input
83
-
84
- except Exception as e:
85
- logger.error(f"Error en análisis: {str(e)}")
86
- st.error(t.get('analysis_error', "Error al analizar el texto"))
87
-
88
- # Mostrar resultados en la columna derecha
89
- with results_col:
90
- if st.session_state.show_results and st.session_state.current_metrics is not None:
91
- display_recommendations(st.session_state.current_metrics, t)
92
-
93
- # Opción para ver detalles
94
- with st.expander("🔍 Ver análisis detallado", expanded=False):
95
- display_current_situation_visual(
96
- st.session_state.current_doc,
97
- st.session_state.current_metrics
98
- )
99
-
100
- def display_current_situation_visual(doc, metrics):
101
- """
102
- Muestra visualizaciones detalladas del análisis.
103
- """
104
- try:
105
- st.markdown("### 📊 Visualizaciones Detalladas")
106
-
107
- # 1. Visualización de vocabulario
108
- with st.expander("Análisis de Vocabulario", expanded=True):
109
- vocab_graph = create_vocabulary_network(doc)
110
- if vocab_graph:
111
- st.pyplot(vocab_graph)
112
- plt.close(vocab_graph)
113
-
114
- # 2. Visualización de estructura
115
- with st.expander("Análisis de Estructura", expanded=True):
116
- syntax_graph = create_syntax_complexity_graph(doc)
117
- if syntax_graph:
118
- st.pyplot(syntax_graph)
119
- plt.close(syntax_graph)
120
-
121
- # 3. Visualización de cohesión
122
- with st.expander("Análisis de Cohesión", expanded=True):
123
- cohesion_graph = create_cohesion_heatmap(doc)
124
- if cohesion_graph:
125
- st.pyplot(cohesion_graph)
126
- plt.close(cohesion_graph)
127
-
128
- except Exception as e:
129
- logger.error(f"Error en visualización: {str(e)}")
130
- st.error("Error al generar las visualizaciones")
131
-
132
-
133
- ####################################
134
- def display_recommendations(metrics, t):
135
- """
136
- Muestra recomendaciones basadas en las métricas del texto.
137
- """
138
- # 1. Resumen Visual con Explicación
139
- st.markdown("### 📊 Resumen de tu Análisis")
140
-
141
- # Explicación del sistema de medición
142
- st.markdown("""
143
- **¿Cómo interpretar los resultados?**
144
-
145
- Cada métrica se mide en una escala de 0.0 a 1.0, donde:
146
- - 0.0 - 0.4: Necesita atención prioritaria
147
- - 0.4 - 0.6: En desarrollo
148
- - 0.6 - 0.8: Buen nivel
149
- - 0.8 - 1.0: Nivel avanzado
150
- """)
151
-
152
- # Métricas con explicaciones detalladas
153
- col1, col2, col3, col4 = st.columns(4)
154
-
155
- with col1:
156
- st.metric(
157
- "Vocabulario",
158
- f"{metrics['vocabulary']['normalized_score']:.2f}",
159
- help="Mide la variedad y riqueza de tu vocabulario. Un valor alto indica un uso diverso de palabras sin repeticiones excesivas."
160
- )
161
- with st.expander("ℹ️ Detalles"):
162
- st.write("""
163
- **Vocabulario**
164
- - Evalúa la diversidad léxica
165
- - Considera palabras únicas vs. totales
166
- - Detecta repeticiones innecesarias
167
- - Valor óptimo: > 0.7
168
- """)
169
-
170
- with col2:
171
- st.metric(
172
- "Estructura",
173
- f"{metrics['structure']['normalized_score']:.2f}",
174
- help="Evalúa la complejidad y variedad de las estructuras sintácticas en tus oraciones."
175
- )
176
- with st.expander("ℹ️ Detalles"):
177
- st.write("""
178
- **Estructura**
179
- - Analiza la complejidad sintáctica
180
- - Mide variación en construcciones
181
- - Evalúa longitud de oraciones
182
- - Valor óptimo: > 0.6
183
- """)
184
-
185
- with col3:
186
- st.metric(
187
- "Cohesión",
188
- f"{metrics['cohesion']['normalized_score']:.2f}",
189
- help="Indica qué tan bien conectadas están tus ideas y párrafos entre sí."
190
- )
191
- with st.expander("ℹ️ Detalles"):
192
- st.write("""
193
- **Cohesión**
194
- - Mide conexiones entre ideas
195
- - Evalúa uso de conectores
196
- - Analiza progresión temática
197
- - Valor óptimo: > 0.65
198
- """)
199
-
200
- with col4:
201
- st.metric(
202
- "Claridad",
203
- f"{metrics['clarity']['normalized_score']:.2f}",
204
- help="Evalúa la facilidad de comprensión general de tu texto."
205
- )
206
- with st.expander("ℹ️ Detalles"):
207
- st.write("""
208
- **Claridad**
209
- - Evalúa comprensibilidad
210
- - Considera estructura lógica
211
- - Mide precisión expresiva
212
- - Valor óptimo: > 0.7
213
- """)
214
-
215
- st.markdown("---")
216
-
217
- # 2. Recomendaciones basadas en puntuaciones
218
- st.markdown("### 💡 Recomendaciones Personalizadas")
219
-
220
- # Recomendaciones morfosintácticas
221
- if metrics['structure']['normalized_score'] < 0.6:
222
- st.warning("""
223
- #### 📝 Análisis Morfosintáctico Recomendado
224
-
225
- **Tu nivel actual sugiere que sería beneficioso:**
226
- 1. Realizar el análisis morfosintáctico de 3 párrafos diferentes
227
- 2. Practicar la combinación de oraciones simples en compuestas
228
- 3. Identificar y clasificar tipos de oraciones en textos académicos
229
- 4. Ejercitar la variación sintáctica
230
-
231
- *Hacer clic en "Comenzar ejercicios" para acceder al módulo morfosintáctico*
232
- """)
233
-
234
- # Recomendaciones semánticas
235
- if metrics['vocabulary']['normalized_score'] < 0.7:
236
- st.warning("""
237
- #### 📚 Análisis Semántico Recomendado
238
-
239
- **Para mejorar tu vocabulario y expresión:**
240
- A. Realiza el análisis semántico de un texto académico
241
- B. Identifica y agrupa campos semánticos relacionados
242
- C. Practica la sustitución léxica en tus párrafos
243
- D. Construye redes de conceptos sobre tu tema
244
- E. Analiza las relaciones entre ideas principales
245
-
246
- *Hacer clic en "Comenzar ejercicios" para acceder al módulo semántico*
247
- """)
248
-
249
- # Recomendaciones de cohesión
250
- if metrics['cohesion']['normalized_score'] < 0.65:
251
- st.warning("""
252
- #### 🔄 Análisis del Discurso Recomendado
253
-
254
- **Para mejorar la conexión entre ideas:**
255
- 1. Realizar el análisis del discurso de un texto modelo
256
- 2. Practicar el uso de diferentes conectores textuales
257
- 3. Identificar cadenas de referencia en textos académicos
258
- 4. Ejercitar la progresión temática en tus escritos
259
-
260
- *Hacer clic en "Comenzar ejercicios" para acceder al módulo de análisis del discurso*
261
- """)
262
-
263
- # Botón de acción
264
- st.markdown("---")
265
- col1, col2, col3 = st.columns([1,2,1])
266
- with col2:
267
- st.button(
268
- "🎯 Comenzar ejercicios recomendados",
269
- type="primary",
270
- use_container_width=True,
271
- key="start_exercises"
272
  )
 
1
+ # modules/studentact/current_situation_interface.py
2
+
3
+ import streamlit as st
4
+ import logging
5
+ from ..utils.widget_utils import generate_unique_key
6
+ from .current_situation_analysis import (
7
+ analyze_text_dimensions,
8
+ analyze_clarity,
9
+ analyze_reference_clarity,
10
+ analyze_vocabulary_diversity,
11
+ analyze_cohesion,
12
+ analyze_structure,
13
+ get_dependency_depths,
14
+ normalize_score,
15
+ generate_sentence_graphs,
16
+ generate_word_connections,
17
+ generate_connection_paths,
18
+ create_vocabulary_network,
19
+ create_syntax_complexity_graph,
20
+ create_cohesion_heatmap,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+ ####################################
25
+ def display_current_situation_interface(lang_code, nlp_models, t):
26
+ """
27
+ Interfaz simplificada para el análisis inicial, enfocada en recomendaciones directas.
28
+ """
29
+ # Inicializar estados si no existen
30
+ if 'text_input' not in st.session_state:
31
+ st.session_state.text_input = ""
32
+ if 'show_results' not in st.session_state:
33
+ st.session_state.show_results = False
34
+ if 'current_doc' not in st.session_state:
35
+ st.session_state.current_doc = None
36
+ if 'current_metrics' not in st.session_state:
37
+ st.session_state.current_metrics = None
38
+
39
+ st.markdown("## Análisis Inicial de Escritura")
40
+
41
+ # Container principal con dos columnas
42
+ with st.container():
43
+ input_col, results_col = st.columns([1,2])
44
+
45
+ with input_col:
46
+ st.markdown("### Ingresa tu texto")
47
+
48
+ # Función para manejar cambios en el texto
49
+ def on_text_change():
50
+ st.session_state.text_input = st.session_state.text_area
51
+ st.session_state.show_results = False # Resetear resultados cuando el texto cambia
52
+
53
+ # Text area con manejo de estado
54
+ text_input = st.text_area(
55
+ t.get('input_prompt', "Escribe o pega tu texto aquí:"),
56
+ height=400,
57
+ key="text_area",
58
+ value=st.session_state.text_input,
59
+ on_change=on_text_change,
60
+ help="Este texto será analizado para darte recomendaciones personalizadas"
61
+ )
62
+
63
+ # Botón de análisis
64
+ if st.button(
65
+ t.get('analyze_button', "Analizar mi escritura"),
66
+ type="primary",
67
+ disabled=not text_input.strip(),
68
+ use_container_width=True,
69
+ ):
70
+ try:
71
+ with st.spinner(t.get('processing', "Analizando...")):
72
+ # Procesar texto y obtener métricas
73
+ doc = nlp_models[lang_code](text_input)
74
+ metrics = analyze_text_dimensions(doc)
75
+
76
+ # Actualizar estado con nuevos resultados
77
+ st.session_state.current_doc = doc
78
+ st.session_state.current_metrics = metrics
79
+ st.session_state.show_results = True
80
+
81
+ # Mantener el texto en el estado
82
+ st.session_state.text_input = text_input
83
+
84
+ except Exception as e:
85
+ logger.error(f"Error en análisis: {str(e)}")
86
+ st.error(t.get('analysis_error', "Error al analizar el texto"))
87
+
88
+ # Mostrar resultados en la columna derecha
89
+ with results_col:
90
+ if st.session_state.show_results and st.session_state.current_metrics is not None:
91
+ display_recommendations(st.session_state.current_metrics, t)
92
+
93
+ # Opción para ver detalles
94
+ with st.expander("🔍 Ver análisis detallado", expanded=False):
95
+ display_current_situation_visual(
96
+ st.session_state.current_doc,
97
+ st.session_state.current_metrics
98
+ )
99
+
100
+ def display_current_situation_visual(doc, metrics):
101
+ """
102
+ Muestra visualizaciones detalladas del análisis.
103
+ """
104
+ try:
105
+ st.markdown("### 📊 Visualizaciones Detalladas")
106
+
107
+ # 1. Visualización de vocabulario
108
+ with st.expander("Análisis de Vocabulario", expanded=True):
109
+ vocab_graph = create_vocabulary_network(doc)
110
+ if vocab_graph:
111
+ st.pyplot(vocab_graph)
112
+ plt.close(vocab_graph)
113
+
114
+ # 2. Visualización de estructura
115
+ with st.expander("Análisis de Estructura", expanded=True):
116
+ syntax_graph = create_syntax_complexity_graph(doc)
117
+ if syntax_graph:
118
+ st.pyplot(syntax_graph)
119
+ plt.close(syntax_graph)
120
+
121
+ # 3. Visualización de cohesión
122
+ with st.expander("Análisis de Cohesión", expanded=True):
123
+ cohesion_graph = create_cohesion_heatmap(doc)
124
+ if cohesion_graph:
125
+ st.pyplot(cohesion_graph)
126
+ plt.close(cohesion_graph)
127
+
128
+ except Exception as e:
129
+ logger.error(f"Error en visualización: {str(e)}")
130
+ st.error("Error al generar las visualizaciones")
131
+
132
+
133
+ ####################################
134
+ def display_recommendations(metrics, t):
135
+ """
136
+ Muestra recomendaciones basadas en las métricas del texto.
137
+ """
138
+ # 1. Resumen Visual con Explicación
139
+ st.markdown("### 📊 Resumen de tu Análisis")
140
+
141
+ # Explicación del sistema de medición
142
+ st.markdown("""
143
+ **¿Cómo interpretar los resultados?**
144
+
145
+ Cada métrica se mide en una escala de 0.0 a 1.0, donde:
146
+ - 0.0 - 0.4: Necesita atención prioritaria
147
+ - 0.4 - 0.6: En desarrollo
148
+ - 0.6 - 0.8: Buen nivel
149
+ - 0.8 - 1.0: Nivel avanzado
150
+ """)
151
+
152
+ # Métricas con explicaciones detalladas
153
+ col1, col2, col3, col4 = st.columns(4)
154
+
155
+ with col1:
156
+ st.metric(
157
+ "Vocabulario",
158
+ f"{metrics['vocabulary']['normalized_score']:.2f}",
159
+ help="Mide la variedad y riqueza de tu vocabulario. Un valor alto indica un uso diverso de palabras sin repeticiones excesivas."
160
+ )
161
+ with st.expander("ℹ️ Detalles"):
162
+ st.write("""
163
+ **Vocabulario**
164
+ - Evalúa la diversidad léxica
165
+ - Considera palabras únicas vs. totales
166
+ - Detecta repeticiones innecesarias
167
+ - Valor óptimo: > 0.7
168
+ """)
169
+
170
+ with col2:
171
+ st.metric(
172
+ "Estructura",
173
+ f"{metrics['structure']['normalized_score']:.2f}",
174
+ help="Evalúa la complejidad y variedad de las estructuras sintácticas en tus oraciones."
175
+ )
176
+ with st.expander("ℹ️ Detalles"):
177
+ st.write("""
178
+ **Estructura**
179
+ - Analiza la complejidad sintáctica
180
+ - Mide variación en construcciones
181
+ - Evalúa longitud de oraciones
182
+ - Valor óptimo: > 0.6
183
+ """)
184
+
185
+ with col3:
186
+ st.metric(
187
+ "Cohesión",
188
+ f"{metrics['cohesion']['normalized_score']:.2f}",
189
+ help="Indica qué tan bien conectadas están tus ideas y párrafos entre sí."
190
+ )
191
+ with st.expander("ℹ️ Detalles"):
192
+ st.write("""
193
+ **Cohesión**
194
+ - Mide conexiones entre ideas
195
+ - Evalúa uso de conectores
196
+ - Analiza progresión temática
197
+ - Valor óptimo: > 0.65
198
+ """)
199
+
200
+ with col4:
201
+ st.metric(
202
+ "Claridad",
203
+ f"{metrics['clarity']['normalized_score']:.2f}",
204
+ help="Evalúa la facilidad de comprensión general de tu texto."
205
+ )
206
+ with st.expander("ℹ️ Detalles"):
207
+ st.write("""
208
+ **Claridad**
209
+ - Evalúa comprensibilidad
210
+ - Considera estructura lógica
211
+ - Mide precisión expresiva
212
+ - Valor óptimo: > 0.7
213
+ """)
214
+
215
+ st.markdown("---")
216
+
217
+ # 2. Recomendaciones basadas en puntuaciones
218
+ st.markdown("### 💡 Recomendaciones Personalizadas")
219
+
220
+ # Recomendaciones morfosintácticas
221
+ if metrics['structure']['normalized_score'] < 0.6:
222
+ st.warning("""
223
+ #### 📝 Análisis Morfosintáctico Recomendado
224
+
225
+ **Tu nivel actual sugiere que sería beneficioso:**
226
+ 1. Realizar el análisis morfosintáctico de 3 párrafos diferentes
227
+ 2. Practicar la combinación de oraciones simples en compuestas
228
+ 3. Identificar y clasificar tipos de oraciones en textos académicos
229
+ 4. Ejercitar la variación sintáctica
230
+
231
+ *Hacer clic en "Comenzar ejercicios" para acceder al módulo morfosintáctico*
232
+ """)
233
+
234
+ # Recomendaciones semánticas
235
+ if metrics['vocabulary']['normalized_score'] < 0.7:
236
+ st.warning("""
237
+ #### 📚 Análisis Semántico Recomendado
238
+
239
+ **Para mejorar tu vocabulario y expresión:**
240
+ A. Realiza el análisis semántico de un texto académico
241
+ B. Identifica y agrupa campos semánticos relacionados
242
+ C. Practica la sustitución léxica en tus párrafos
243
+ D. Construye redes de conceptos sobre tu tema
244
+ E. Analiza las relaciones entre ideas principales
245
+
246
+ *Hacer clic en "Comenzar ejercicios" para acceder al módulo semántico*
247
+ """)
248
+
249
+ # Recomendaciones de cohesión
250
+ if metrics['cohesion']['normalized_score'] < 0.65:
251
+ st.warning("""
252
+ #### 🔄 Análisis del Discurso Recomendado
253
+
254
+ **Para mejorar la conexión entre ideas:**
255
+ 1. Realizar el análisis del discurso de un texto modelo
256
+ 2. Practicar el uso de diferentes conectores textuales
257
+ 3. Identificar cadenas de referencia en textos académicos
258
+ 4. Ejercitar la progresión temática en tus escritos
259
+
260
+ *Hacer clic en "Comenzar ejercicios" para acceder al módulo de análisis del discurso*
261
+ """)
262
+
263
+ # Botón de acción
264
+ st.markdown("---")
265
+ col1, col2, col3 = st.columns([1,2,1])
266
+ with col2:
267
+ st.button(
268
+ "🎯 Comenzar ejercicios recomendados",
269
+ type="primary",
270
+ use_container_width=True,
271
+ key="start_exercises"
272
  )
modules/studentact/current_situation_interface-v2.py CHANGED
@@ -1,291 +1,291 @@
1
- # modules/studentact/current_situation_interface.py
2
-
3
- import streamlit as st
4
- import logging
5
- from ..utils.widget_utils import generate_unique_key
6
-
7
- from ..database.current_situation_mongo_db import store_current_situation_result
8
-
9
- from .current_situation_analysis import (
10
- analyze_text_dimensions,
11
- analyze_clarity,
12
- analyze_reference_clarity,
13
- analyze_vocabulary_diversity,
14
- analyze_cohesion,
15
- analyze_structure,
16
- get_dependency_depths,
17
- normalize_score,
18
- generate_sentence_graphs,
19
- generate_word_connections,
20
- generate_connection_paths,
21
- create_vocabulary_network,
22
- create_syntax_complexity_graph,
23
- create_cohesion_heatmap,
24
- )
25
-
26
- logger = logging.getLogger(__name__)
27
- ####################################
28
-
29
- def display_current_situation_interface(lang_code, nlp_models, t):
30
- """
31
- Interfaz simplificada para el análisis inicial, enfocada en recomendaciones directas.
32
- """
33
- try:
34
- # Inicializar estados si no existen
35
- if 'text_input' not in st.session_state:
36
- st.session_state.text_input = ""
37
- if 'show_results' not in st.session_state:
38
- st.session_state.show_results = False
39
- if 'current_doc' not in st.session_state:
40
- st.session_state.current_doc = None
41
- if 'current_metrics' not in st.session_state:
42
- st.session_state.current_metrics = None
43
-
44
- st.markdown("## Análisis Inicial de Escritura")
45
-
46
- # Container principal con dos columnas
47
- with st.container():
48
- input_col, results_col = st.columns([1,2])
49
-
50
- with input_col:
51
- st.markdown("### Ingresa tu texto")
52
-
53
- # Función para manejar cambios en el texto
54
- def on_text_change():
55
- st.session_state.text_input = st.session_state.text_area
56
- st.session_state.show_results = False # Resetear resultados cuando el texto cambia
57
-
58
- # Text area con manejo de estado
59
- text_input = st.text_area(
60
- t.get('input_prompt', "Escribe o pega tu texto aquí:"),
61
- height=400,
62
- key="text_area",
63
- value=st.session_state.text_input,
64
- on_change=on_text_change,
65
- help="Este texto será analizado para darte recomendaciones personalizadas"
66
- )
67
-
68
- if st.button(
69
- t.get('analyze_button', "Analizar mi escritura"),
70
- type="primary",
71
- disabled=not text_input.strip(),
72
- use_container_width=True,
73
- ):
74
- try:
75
- with st.spinner(t.get('processing', "Analizando...")):
76
- # Procesar texto y obtener métricas
77
- doc = nlp_models[lang_code](text_input)
78
- metrics = analyze_text_dimensions(doc)
79
-
80
- # Guardar en MongoDB
81
- storage_success = store_current_situation_result(
82
- username=st.session_state.username,
83
- text=text_input,
84
- metrics=metrics,
85
- feedback=None # Por ahora sin feedback
86
- )
87
-
88
- if not storage_success:
89
- logger.warning("No se pudo guardar el análisis en la base de datos")
90
-
91
- # Actualizar estado
92
- st.session_state.current_doc = doc
93
- st.session_state.current_metrics = metrics
94
- st.session_state.show_results = True
95
- st.session_state.text_input = text_input
96
-
97
- except Exception as e:
98
- logger.error(f"Error en análisis: {str(e)}")
99
- st.error(t.get('analysis_error', "Error al analizar el texto"))
100
-
101
- # Mostrar resultados en la columna derecha
102
- with results_col:
103
- if st.session_state.show_results and st.session_state.current_metrics is not None:
104
- display_recommendations(st.session_state.current_metrics, t)
105
-
106
- # Opción para ver detalles
107
- with st.expander("🔍 Ver análisis detallado", expanded=False):
108
- display_current_situation_visual(
109
- st.session_state.current_doc,
110
- st.session_state.current_metrics
111
- )
112
-
113
- except Exception as e:
114
- logger.error(f"Error en interfaz: {str(e)}")
115
- st.error("Ocurrió un error. Por favor, intente de nuevo.")
116
-
117
-
118
-
119
- def display_current_situation_visual(doc, metrics):
120
- """
121
- Muestra visualizaciones detalladas del análisis.
122
- """
123
- try:
124
- st.markdown("### 📊 Visualizaciones Detalladas")
125
-
126
- # 1. Visualización de vocabulario
127
- with st.expander("Análisis de Vocabulario", expanded=True):
128
- vocab_graph = create_vocabulary_network(doc)
129
- if vocab_graph:
130
- st.pyplot(vocab_graph)
131
- plt.close(vocab_graph)
132
-
133
- # 2. Visualización de estructura
134
- with st.expander("Análisis de Estructura", expanded=True):
135
- syntax_graph = create_syntax_complexity_graph(doc)
136
- if syntax_graph:
137
- st.pyplot(syntax_graph)
138
- plt.close(syntax_graph)
139
-
140
- # 3. Visualización de cohesión
141
- with st.expander("Análisis de Cohesión", expanded=True):
142
- cohesion_graph = create_cohesion_heatmap(doc)
143
- if cohesion_graph:
144
- st.pyplot(cohesion_graph)
145
- plt.close(cohesion_graph)
146
-
147
- except Exception as e:
148
- logger.error(f"Error en visualización: {str(e)}")
149
- st.error("Error al generar las visualizaciones")
150
-
151
-
152
- ####################################
153
- def display_recommendations(metrics, t):
154
- """
155
- Muestra recomendaciones basadas en las métricas del texto.
156
- """
157
- # 1. Resumen Visual con Explicación
158
- st.markdown("### 📊 Resumen de tu Análisis")
159
-
160
- # Explicación del sistema de medición
161
- st.markdown("""
162
- **¿Cómo interpretar los resultados?**
163
-
164
- Cada métrica se mide en una escala de 0.0 a 1.0, donde:
165
- - 0.0 - 0.4: Necesita atención prioritaria
166
- - 0.4 - 0.6: En desarrollo
167
- - 0.6 - 0.8: Buen nivel
168
- - 0.8 - 1.0: Nivel avanzado
169
- """)
170
-
171
- # Métricas con explicaciones detalladas
172
- col1, col2, col3, col4 = st.columns(4)
173
-
174
- with col1:
175
- st.metric(
176
- "Vocabulario",
177
- f"{metrics['vocabulary']['normalized_score']:.2f}",
178
- help="Mide la variedad y riqueza de tu vocabulario. Un valor alto indica un uso diverso de palabras sin repeticiones excesivas."
179
- )
180
- with st.expander("ℹ️ Detalles"):
181
- st.write("""
182
- **Vocabulario**
183
- - Evalúa la diversidad léxica
184
- - Considera palabras únicas vs. totales
185
- - Detecta repeticiones innecesarias
186
- - Valor óptimo: > 0.7
187
- """)
188
-
189
- with col2:
190
- st.metric(
191
- "Estructura",
192
- f"{metrics['structure']['normalized_score']:.2f}",
193
- help="Evalúa la complejidad y variedad de las estructuras sintácticas en tus oraciones."
194
- )
195
- with st.expander("ℹ️ Detalles"):
196
- st.write("""
197
- **Estructura**
198
- - Analiza la complejidad sintáctica
199
- - Mide variación en construcciones
200
- - Evalúa longitud de oraciones
201
- - Valor óptimo: > 0.6
202
- """)
203
-
204
- with col3:
205
- st.metric(
206
- "Cohesión",
207
- f"{metrics['cohesion']['normalized_score']:.2f}",
208
- help="Indica qué tan bien conectadas están tus ideas y párrafos entre sí."
209
- )
210
- with st.expander("ℹ️ Detalles"):
211
- st.write("""
212
- **Cohesión**
213
- - Mide conexiones entre ideas
214
- - Evalúa uso de conectores
215
- - Analiza progresión temática
216
- - Valor óptimo: > 0.65
217
- """)
218
-
219
- with col4:
220
- st.metric(
221
- "Claridad",
222
- f"{metrics['clarity']['normalized_score']:.2f}",
223
- help="Evalúa la facilidad de comprensión general de tu texto."
224
- )
225
- with st.expander("ℹ️ Detalles"):
226
- st.write("""
227
- **Claridad**
228
- - Evalúa comprensibilidad
229
- - Considera estructura lógica
230
- - Mide precisión expresiva
231
- - Valor óptimo: > 0.7
232
- """)
233
-
234
- st.markdown("---")
235
-
236
- # 2. Recomendaciones basadas en puntuaciones
237
- st.markdown("### 💡 Recomendaciones Personalizadas")
238
-
239
- # Recomendaciones morfosintácticas
240
- if metrics['structure']['normalized_score'] < 0.6:
241
- st.warning("""
242
- #### 📝 Análisis Morfosintáctico Recomendado
243
-
244
- **Tu nivel actual sugiere que sería beneficioso:**
245
- 1. Realizar el análisis morfosintáctico de 3 párrafos diferentes
246
- 2. Practicar la combinación de oraciones simples en compuestas
247
- 3. Identificar y clasificar tipos de oraciones en textos académicos
248
- 4. Ejercitar la variación sintáctica
249
-
250
- *Hacer clic en "Comenzar ejercicios" para acceder al módulo morfosintáctico*
251
- """)
252
-
253
- # Recomendaciones semánticas
254
- if metrics['vocabulary']['normalized_score'] < 0.7:
255
- st.warning("""
256
- #### 📚 Análisis Semántico Recomendado
257
-
258
- **Para mejorar tu vocabulario y expresión:**
259
- A. Realiza el análisis semántico de un texto académico
260
- B. Identifica y agrupa campos semánticos relacionados
261
- C. Practica la sustitución léxica en tus párrafos
262
- D. Construye redes de conceptos sobre tu tema
263
- E. Analiza las relaciones entre ideas principales
264
-
265
- *Hacer clic en "Comenzar ejercicios" para acceder al módulo semántico*
266
- """)
267
-
268
- # Recomendaciones de cohesión
269
- if metrics['cohesion']['normalized_score'] < 0.65:
270
- st.warning("""
271
- #### 🔄 Análisis del Discurso Recomendado
272
-
273
- **Para mejorar la conexión entre ideas:**
274
- 1. Realizar el análisis del discurso de un texto modelo
275
- 2. Practicar el uso de diferentes conectores textuales
276
- 3. Identificar cadenas de referencia en textos académicos
277
- 4. Ejercitar la progresión temática en tus escritos
278
-
279
- *Hacer clic en "Comenzar ejercicios" para acceder al módulo de análisis del discurso*
280
- """)
281
-
282
- # Botón de acción
283
- st.markdown("---")
284
- col1, col2, col3 = st.columns([1,2,1])
285
- with col2:
286
- st.button(
287
- "🎯 Comenzar ejercicios recomendados",
288
- type="primary",
289
- use_container_width=True,
290
- key="start_exercises"
291
- )
 
1
+ # modules/studentact/current_situation_interface.py
2
+
3
+ import streamlit as st
4
+ import logging
5
+ from ..utils.widget_utils import generate_unique_key
6
+
7
+ from ..database.current_situation_mongo_db import store_current_situation_result
8
+
9
+ from .current_situation_analysis import (
10
+ analyze_text_dimensions,
11
+ analyze_clarity,
12
+ analyze_reference_clarity,
13
+ analyze_vocabulary_diversity,
14
+ analyze_cohesion,
15
+ analyze_structure,
16
+ get_dependency_depths,
17
+ normalize_score,
18
+ generate_sentence_graphs,
19
+ generate_word_connections,
20
+ generate_connection_paths,
21
+ create_vocabulary_network,
22
+ create_syntax_complexity_graph,
23
+ create_cohesion_heatmap,
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+ ####################################
28
+
29
+ def display_current_situation_interface(lang_code, nlp_models, t):
30
+ """
31
+ Interfaz simplificada para el análisis inicial, enfocada en recomendaciones directas.
32
+ """
33
+ try:
34
+ # Inicializar estados si no existen
35
+ if 'text_input' not in st.session_state:
36
+ st.session_state.text_input = ""
37
+ if 'show_results' not in st.session_state:
38
+ st.session_state.show_results = False
39
+ if 'current_doc' not in st.session_state:
40
+ st.session_state.current_doc = None
41
+ if 'current_metrics' not in st.session_state:
42
+ st.session_state.current_metrics = None
43
+
44
+ st.markdown("## Análisis Inicial de Escritura")
45
+
46
+ # Container principal con dos columnas
47
+ with st.container():
48
+ input_col, results_col = st.columns([1,2])
49
+
50
+ with input_col:
51
+ st.markdown("### Ingresa tu texto")
52
+
53
+ # Función para manejar cambios en el texto
54
+ def on_text_change():
55
+ st.session_state.text_input = st.session_state.text_area
56
+ st.session_state.show_results = False # Resetear resultados cuando el texto cambia
57
+
58
+ # Text area con manejo de estado
59
+ text_input = st.text_area(
60
+ t.get('input_prompt', "Escribe o pega tu texto aquí:"),
61
+ height=400,
62
+ key="text_area",
63
+ value=st.session_state.text_input,
64
+ on_change=on_text_change,
65
+ help="Este texto será analizado para darte recomendaciones personalizadas"
66
+ )
67
+
68
+ if st.button(
69
+ t.get('analyze_button', "Analizar mi escritura"),
70
+ type="primary",
71
+ disabled=not text_input.strip(),
72
+ use_container_width=True,
73
+ ):
74
+ try:
75
+ with st.spinner(t.get('processing', "Analizando...")):
76
+ # Procesar texto y obtener métricas
77
+ doc = nlp_models[lang_code](text_input)
78
+ metrics = analyze_text_dimensions(doc)
79
+
80
+ # Guardar en MongoDB
81
+ storage_success = store_current_situation_result(
82
+ username=st.session_state.username,
83
+ text=text_input,
84
+ metrics=metrics,
85
+ feedback=None # Por ahora sin feedback
86
+ )
87
+
88
+ if not storage_success:
89
+ logger.warning("No se pudo guardar el análisis en la base de datos")
90
+
91
+ # Actualizar estado
92
+ st.session_state.current_doc = doc
93
+ st.session_state.current_metrics = metrics
94
+ st.session_state.show_results = True
95
+ st.session_state.text_input = text_input
96
+
97
+ except Exception as e:
98
+ logger.error(f"Error en análisis: {str(e)}")
99
+ st.error(t.get('analysis_error', "Error al analizar el texto"))
100
+
101
+ # Mostrar resultados en la columna derecha
102
+ with results_col:
103
+ if st.session_state.show_results and st.session_state.current_metrics is not None:
104
+ display_recommendations(st.session_state.current_metrics, t)
105
+
106
+ # Opción para ver detalles
107
+ with st.expander("🔍 Ver análisis detallado", expanded=False):
108
+ display_current_situation_visual(
109
+ st.session_state.current_doc,
110
+ st.session_state.current_metrics
111
+ )
112
+
113
+ except Exception as e:
114
+ logger.error(f"Error en interfaz: {str(e)}")
115
+ st.error("Ocurrió un error. Por favor, intente de nuevo.")
116
+
117
+
118
+
119
+ def display_current_situation_visual(doc, metrics):
120
+ """
121
+ Muestra visualizaciones detalladas del análisis.
122
+ """
123
+ try:
124
+ st.markdown("### 📊 Visualizaciones Detalladas")
125
+
126
+ # 1. Visualización de vocabulario
127
+ with st.expander("Análisis de Vocabulario", expanded=True):
128
+ vocab_graph = create_vocabulary_network(doc)
129
+ if vocab_graph:
130
+ st.pyplot(vocab_graph)
131
+ plt.close(vocab_graph)
132
+
133
+ # 2. Visualización de estructura
134
+ with st.expander("Análisis de Estructura", expanded=True):
135
+ syntax_graph = create_syntax_complexity_graph(doc)
136
+ if syntax_graph:
137
+ st.pyplot(syntax_graph)
138
+ plt.close(syntax_graph)
139
+
140
+ # 3. Visualización de cohesión
141
+ with st.expander("Análisis de Cohesión", expanded=True):
142
+ cohesion_graph = create_cohesion_heatmap(doc)
143
+ if cohesion_graph:
144
+ st.pyplot(cohesion_graph)
145
+ plt.close(cohesion_graph)
146
+
147
+ except Exception as e:
148
+ logger.error(f"Error en visualización: {str(e)}")
149
+ st.error("Error al generar las visualizaciones")
150
+
151
+
152
+ ####################################
153
+ def display_recommendations(metrics, t):
154
+ """
155
+ Muestra recomendaciones basadas en las métricas del texto.
156
+ """
157
+ # 1. Resumen Visual con Explicación
158
+ st.markdown("### 📊 Resumen de tu Análisis")
159
+
160
+ # Explicación del sistema de medición
161
+ st.markdown("""
162
+ **¿Cómo interpretar los resultados?**
163
+
164
+ Cada métrica se mide en una escala de 0.0 a 1.0, donde:
165
+ - 0.0 - 0.4: Necesita atención prioritaria
166
+ - 0.4 - 0.6: En desarrollo
167
+ - 0.6 - 0.8: Buen nivel
168
+ - 0.8 - 1.0: Nivel avanzado
169
+ """)
170
+
171
+ # Métricas con explicaciones detalladas
172
+ col1, col2, col3, col4 = st.columns(4)
173
+
174
+ with col1:
175
+ st.metric(
176
+ "Vocabulario",
177
+ f"{metrics['vocabulary']['normalized_score']:.2f}",
178
+ help="Mide la variedad y riqueza de tu vocabulario. Un valor alto indica un uso diverso de palabras sin repeticiones excesivas."
179
+ )
180
+ with st.expander("ℹ️ Detalles"):
181
+ st.write("""
182
+ **Vocabulario**
183
+ - Evalúa la diversidad léxica
184
+ - Considera palabras únicas vs. totales
185
+ - Detecta repeticiones innecesarias
186
+ - Valor óptimo: > 0.7
187
+ """)
188
+
189
+ with col2:
190
+ st.metric(
191
+ "Estructura",
192
+ f"{metrics['structure']['normalized_score']:.2f}",
193
+ help="Evalúa la complejidad y variedad de las estructuras sintácticas en tus oraciones."
194
+ )
195
+ with st.expander("ℹ️ Detalles"):
196
+ st.write("""
197
+ **Estructura**
198
+ - Analiza la complejidad sintáctica
199
+ - Mide variación en construcciones
200
+ - Evalúa longitud de oraciones
201
+ - Valor óptimo: > 0.6
202
+ """)
203
+
204
+ with col3:
205
+ st.metric(
206
+ "Cohesión",
207
+ f"{metrics['cohesion']['normalized_score']:.2f}",
208
+ help="Indica qué tan bien conectadas están tus ideas y párrafos entre sí."
209
+ )
210
+ with st.expander("ℹ️ Detalles"):
211
+ st.write("""
212
+ **Cohesión**
213
+ - Mide conexiones entre ideas
214
+ - Evalúa uso de conectores
215
+ - Analiza progresión temática
216
+ - Valor óptimo: > 0.65
217
+ """)
218
+
219
+ with col4:
220
+ st.metric(
221
+ "Claridad",
222
+ f"{metrics['clarity']['normalized_score']:.2f}",
223
+ help="Evalúa la facilidad de comprensión general de tu texto."
224
+ )
225
+ with st.expander("ℹ️ Detalles"):
226
+ st.write("""
227
+ **Claridad**
228
+ - Evalúa comprensibilidad
229
+ - Considera estructura lógica
230
+ - Mide precisión expresiva
231
+ - Valor óptimo: > 0.7
232
+ """)
233
+
234
+ st.markdown("---")
235
+
236
+ # 2. Recomendaciones basadas en puntuaciones
237
+ st.markdown("### 💡 Recomendaciones Personalizadas")
238
+
239
+ # Recomendaciones morfosintácticas
240
+ if metrics['structure']['normalized_score'] < 0.6:
241
+ st.warning("""
242
+ #### 📝 Análisis Morfosintáctico Recomendado
243
+
244
+ **Tu nivel actual sugiere que sería beneficioso:**
245
+ 1. Realizar el análisis morfosintáctico de 3 párrafos diferentes
246
+ 2. Practicar la combinación de oraciones simples en compuestas
247
+ 3. Identificar y clasificar tipos de oraciones en textos académicos
248
+ 4. Ejercitar la variación sintáctica
249
+
250
+ *Hacer clic en "Comenzar ejercicios" para acceder al módulo morfosintáctico*
251
+ """)
252
+
253
+ # Recomendaciones semánticas
254
+ if metrics['vocabulary']['normalized_score'] < 0.7:
255
+ st.warning("""
256
+ #### 📚 Análisis Semántico Recomendado
257
+
258
+ **Para mejorar tu vocabulario y expresión:**
259
+ A. Realiza el análisis semántico de un texto académico
260
+ B. Identifica y agrupa campos semánticos relacionados
261
+ C. Practica la sustitución léxica en tus párrafos
262
+ D. Construye redes de conceptos sobre tu tema
263
+ E. Analiza las relaciones entre ideas principales
264
+
265
+ *Hacer clic en "Comenzar ejercicios" para acceder al módulo semántico*
266
+ """)
267
+
268
+ # Recomendaciones de cohesión
269
+ if metrics['cohesion']['normalized_score'] < 0.65:
270
+ st.warning("""
271
+ #### 🔄 Análisis del Discurso Recomendado
272
+
273
+ **Para mejorar la conexión entre ideas:**
274
+ 1. Realizar el análisis del discurso de un texto modelo
275
+ 2. Practicar el uso de diferentes conectores textuales
276
+ 3. Identificar cadenas de referencia en textos académicos
277
+ 4. Ejercitar la progresión temática en tus escritos
278
+
279
+ *Hacer clic en "Comenzar ejercicios" para acceder al módulo de análisis del discurso*
280
+ """)
281
+
282
+ # Botón de acción
283
+ st.markdown("---")
284
+ col1, col2, col3 = st.columns([1,2,1])
285
+ with col2:
286
+ st.button(
287
+ "🎯 Comenzar ejercicios recomendados",
288
+ type="primary",
289
+ use_container_width=True,
290
+ key="start_exercises"
291
+ )
modules/studentact/current_situation_interface-v3.py CHANGED
@@ -1,190 +1,190 @@
1
- # modules/studentact/current_situation_interface.py
2
-
3
- import streamlit as st
4
- import logging
5
- from ..utils.widget_utils import generate_unique_key
6
- import matplotlib.pyplot as plt
7
- import numpy as np
8
- from ..database.current_situation_mongo_db import store_current_situation_result
9
-
10
- from .current_situation_analysis import (
11
- analyze_text_dimensions,
12
- analyze_clarity,
13
- analyze_reference_clarity,
14
- analyze_vocabulary_diversity,
15
- analyze_cohesion,
16
- analyze_structure,
17
- get_dependency_depths,
18
- normalize_score,
19
- generate_sentence_graphs,
20
- generate_word_connections,
21
- generate_connection_paths,
22
- create_vocabulary_network,
23
- create_syntax_complexity_graph,
24
- create_cohesion_heatmap,
25
- )
26
-
27
- # Configuración del estilo de matplotlib para el gráfico de radar
28
- plt.rcParams['font.family'] = 'sans-serif'
29
- plt.rcParams['axes.grid'] = True
30
- plt.rcParams['axes.spines.top'] = False
31
- plt.rcParams['axes.spines.right'] = False
32
-
33
- logger = logging.getLogger(__name__)
34
- ####################################
35
-
36
- def display_current_situation_interface(lang_code, nlp_models, t):
37
- """
38
- Interfaz simplificada con gráfico de radar para visualizar métricas.
39
- """
40
- try:
41
- # Inicializar estados si no existen
42
- if 'text_input' not in st.session_state:
43
- st.session_state.text_input = ""
44
- if 'show_results' not in st.session_state:
45
- st.session_state.show_results = False
46
- if 'current_doc' not in st.session_state:
47
- st.session_state.current_doc = None
48
- if 'current_metrics' not in st.session_state:
49
- st.session_state.current_metrics = None
50
-
51
- st.markdown("## Análisis Inicial de Escritura")
52
-
53
- # Container principal con dos columnas
54
- with st.container():
55
- input_col, results_col = st.columns([1,2])
56
-
57
- with input_col:
58
- #st.markdown("### Ingresa tu texto")
59
-
60
- # Función para manejar cambios en el texto
61
- def on_text_change():
62
- st.session_state.text_input = st.session_state.text_area
63
- st.session_state.show_results = False
64
-
65
- # Text area con manejo de estado
66
- text_input = st.text_area(
67
- t.get('input_prompt', "Escribe o pega tu texto aquí:"),
68
- height=400,
69
- key="text_area",
70
- value=st.session_state.text_input,
71
- on_change=on_text_change,
72
- help="Este texto será analizado para darte recomendaciones personalizadas"
73
- )
74
-
75
- if st.button(
76
- t.get('analyze_button', "Analizar mi escritura"),
77
- type="primary",
78
- disabled=not text_input.strip(),
79
- use_container_width=True,
80
- ):
81
- try:
82
- with st.spinner(t.get('processing', "Analizando...")):
83
- doc = nlp_models[lang_code](text_input)
84
- metrics = analyze_text_dimensions(doc)
85
-
86
- # Guardar en MongoDB
87
- storage_success = store_current_situation_result(
88
- username=st.session_state.username,
89
- text=text_input,
90
- metrics=metrics,
91
- feedback=None
92
- )
93
-
94
- if not storage_success:
95
- logger.warning("No se pudo guardar el análisis en la base de datos")
96
-
97
- st.session_state.current_doc = doc
98
- st.session_state.current_metrics = metrics
99
- st.session_state.show_results = True
100
- st.session_state.text_input = text_input
101
-
102
- except Exception as e:
103
- logger.error(f"Error en análisis: {str(e)}")
104
- st.error(t.get('analysis_error', "Error al analizar el texto"))
105
-
106
- # Mostrar resultados en la columna derecha
107
- with results_col:
108
- if st.session_state.show_results and st.session_state.current_metrics is not None:
109
- display_radar_chart(st.session_state.current_metrics)
110
-
111
- except Exception as e:
112
- logger.error(f"Error en interfaz: {str(e)}")
113
- st.error("Ocurrió un error. Por favor, intente de nuevo.")
114
-
115
- def display_radar_chart(metrics):
116
- """
117
- Muestra un gráfico de radar con las métricas del usuario y el patrón ideal.
118
- """
119
- try:
120
- # Container con proporción reducida
121
- with st.container():
122
- # Métricas en la parte superior
123
- col1, col2, col3, col4 = st.columns(4)
124
- with col1:
125
- st.metric("Vocabulario", f"{metrics['vocabulary']['normalized_score']:.2f}", "1.00")
126
- with col2:
127
- st.metric("Estructura", f"{metrics['structure']['normalized_score']:.2f}", "1.00")
128
- with col3:
129
- st.metric("Cohesión", f"{metrics['cohesion']['normalized_score']:.2f}", "1.00")
130
- with col4:
131
- st.metric("Claridad", f"{metrics['clarity']['normalized_score']:.2f}", "1.00")
132
-
133
- # Contenedor para el gráfico con ancho controlado
134
- _, graph_col, _ = st.columns([1,2,1])
135
-
136
- with graph_col:
137
- # Preparar datos
138
- categories = ['Vocabulario', 'Estructura', 'Cohesión', 'Claridad']
139
- values_user = [
140
- metrics['vocabulary']['normalized_score'],
141
- metrics['structure']['normalized_score'],
142
- metrics['cohesion']['normalized_score'],
143
- metrics['clarity']['normalized_score']
144
- ]
145
- values_pattern = [1.0, 1.0, 1.0, 1.0] # Patrón ideal
146
-
147
- # Crear figura más compacta
148
- fig = plt.figure(figsize=(6, 6))
149
- ax = fig.add_subplot(111, projection='polar')
150
-
151
- # Número de variables
152
- num_vars = len(categories)
153
-
154
- # Calcular ángulos
155
- angles = [n / float(num_vars) * 2 * np.pi for n in range(num_vars)]
156
- angles += angles[:1]
157
-
158
- # Extender valores para cerrar polígonos
159
- values_user += values_user[:1]
160
- values_pattern += values_pattern[:1]
161
-
162
- # Configurar ejes y etiquetas
163
- ax.set_xticks(angles[:-1])
164
- ax.set_xticklabels(categories, fontsize=8)
165
-
166
- # Círculos concéntricos y etiquetas
167
- circle_ticks = np.arange(0, 1.1, 0.2) # Reducido a 5 niveles
168
- ax.set_yticks(circle_ticks)
169
- ax.set_yticklabels([f'{tick:.1f}' for tick in circle_ticks], fontsize=8)
170
- ax.set_ylim(0, 1)
171
-
172
- # Dibujar patrón ideal
173
- ax.plot(angles, values_pattern, 'g--', linewidth=1, label='Patrón', alpha=0.5)
174
- ax.fill(angles, values_pattern, 'g', alpha=0.1)
175
-
176
- # Dibujar valores del usuario
177
- ax.plot(angles, values_user, 'b-', linewidth=2, label='Tu escritura')
178
- ax.fill(angles, values_user, 'b', alpha=0.2)
179
-
180
- # Leyenda
181
- ax.legend(loc='upper right', bbox_to_anchor=(0.1, 0.1), fontsize=8)
182
-
183
- # Ajustes finales
184
- plt.tight_layout()
185
- st.pyplot(fig)
186
- plt.close()
187
-
188
- except Exception as e:
189
- logger.error(f"Error generando gráfico de radar: {str(e)}")
190
  st.error("Error al generar la visualización")
 
1
+ # modules/studentact/current_situation_interface.py
2
+
3
+ import streamlit as st
4
+ import logging
5
+ from ..utils.widget_utils import generate_unique_key
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ from ..database.current_situation_mongo_db import store_current_situation_result
9
+
10
+ from .current_situation_analysis import (
11
+ analyze_text_dimensions,
12
+ analyze_clarity,
13
+ analyze_reference_clarity,
14
+ analyze_vocabulary_diversity,
15
+ analyze_cohesion,
16
+ analyze_structure,
17
+ get_dependency_depths,
18
+ normalize_score,
19
+ generate_sentence_graphs,
20
+ generate_word_connections,
21
+ generate_connection_paths,
22
+ create_vocabulary_network,
23
+ create_syntax_complexity_graph,
24
+ create_cohesion_heatmap,
25
+ )
26
+
27
+ # Configuración del estilo de matplotlib para el gráfico de radar
28
+ plt.rcParams['font.family'] = 'sans-serif'
29
+ plt.rcParams['axes.grid'] = True
30
+ plt.rcParams['axes.spines.top'] = False
31
+ plt.rcParams['axes.spines.right'] = False
32
+
33
+ logger = logging.getLogger(__name__)
34
+ ####################################
35
+
36
+ def display_current_situation_interface(lang_code, nlp_models, t):
37
+ """
38
+ Interfaz simplificada con gráfico de radar para visualizar métricas.
39
+ """
40
+ try:
41
+ # Inicializar estados si no existen
42
+ if 'text_input' not in st.session_state:
43
+ st.session_state.text_input = ""
44
+ if 'show_results' not in st.session_state:
45
+ st.session_state.show_results = False
46
+ if 'current_doc' not in st.session_state:
47
+ st.session_state.current_doc = None
48
+ if 'current_metrics' not in st.session_state:
49
+ st.session_state.current_metrics = None
50
+
51
+ st.markdown("## Análisis Inicial de Escritura")
52
+
53
+ # Container principal con dos columnas
54
+ with st.container():
55
+ input_col, results_col = st.columns([1,2])
56
+
57
+ with input_col:
58
+ #st.markdown("### Ingresa tu texto")
59
+
60
+ # Función para manejar cambios en el texto
61
+ def on_text_change():
62
+ st.session_state.text_input = st.session_state.text_area
63
+ st.session_state.show_results = False
64
+
65
+ # Text area con manejo de estado
66
+ text_input = st.text_area(
67
+ t.get('input_prompt', "Escribe o pega tu texto aquí:"),
68
+ height=400,
69
+ key="text_area",
70
+ value=st.session_state.text_input,
71
+ on_change=on_text_change,
72
+ help="Este texto será analizado para darte recomendaciones personalizadas"
73
+ )
74
+
75
+ if st.button(
76
+ t.get('analyze_button', "Analizar mi escritura"),
77
+ type="primary",
78
+ disabled=not text_input.strip(),
79
+ use_container_width=True,
80
+ ):
81
+ try:
82
+ with st.spinner(t.get('processing', "Analizando...")):
83
+ doc = nlp_models[lang_code](text_input)
84
+ metrics = analyze_text_dimensions(doc)
85
+
86
+ # Guardar en MongoDB
87
+ storage_success = store_current_situation_result(
88
+ username=st.session_state.username,
89
+ text=text_input,
90
+ metrics=metrics,
91
+ feedback=None
92
+ )
93
+
94
+ if not storage_success:
95
+ logger.warning("No se pudo guardar el análisis en la base de datos")
96
+
97
+ st.session_state.current_doc = doc
98
+ st.session_state.current_metrics = metrics
99
+ st.session_state.show_results = True
100
+ st.session_state.text_input = text_input
101
+
102
+ except Exception as e:
103
+ logger.error(f"Error en análisis: {str(e)}")
104
+ st.error(t.get('analysis_error', "Error al analizar el texto"))
105
+
106
+ # Mostrar resultados en la columna derecha
107
+ with results_col:
108
+ if st.session_state.show_results and st.session_state.current_metrics is not None:
109
+ display_radar_chart(st.session_state.current_metrics)
110
+
111
+ except Exception as e:
112
+ logger.error(f"Error en interfaz: {str(e)}")
113
+ st.error("Ocurrió un error. Por favor, intente de nuevo.")
114
+
115
+ def display_radar_chart(metrics):
116
+ """
117
+ Muestra un gráfico de radar con las métricas del usuario y el patrón ideal.
118
+ """
119
+ try:
120
+ # Container con proporción reducida
121
+ with st.container():
122
+ # Métricas en la parte superior
123
+ col1, col2, col3, col4 = st.columns(4)
124
+ with col1:
125
+ st.metric("Vocabulario", f"{metrics['vocabulary']['normalized_score']:.2f}", "1.00")
126
+ with col2:
127
+ st.metric("Estructura", f"{metrics['structure']['normalized_score']:.2f}", "1.00")
128
+ with col3:
129
+ st.metric("Cohesión", f"{metrics['cohesion']['normalized_score']:.2f}", "1.00")
130
+ with col4:
131
+ st.metric("Claridad", f"{metrics['clarity']['normalized_score']:.2f}", "1.00")
132
+
133
+ # Contenedor para el gráfico con ancho controlado
134
+ _, graph_col, _ = st.columns([1,2,1])
135
+
136
+ with graph_col:
137
+ # Preparar datos
138
+ categories = ['Vocabulario', 'Estructura', 'Cohesión', 'Claridad']
139
+ values_user = [
140
+ metrics['vocabulary']['normalized_score'],
141
+ metrics['structure']['normalized_score'],
142
+ metrics['cohesion']['normalized_score'],
143
+ metrics['clarity']['normalized_score']
144
+ ]
145
+ values_pattern = [1.0, 1.0, 1.0, 1.0] # Patrón ideal
146
+
147
+ # Crear figura más compacta
148
+ fig = plt.figure(figsize=(6, 6))
149
+ ax = fig.add_subplot(111, projection='polar')
150
+
151
+ # Número de variables
152
+ num_vars = len(categories)
153
+
154
+ # Calcular ángulos
155
+ angles = [n / float(num_vars) * 2 * np.pi for n in range(num_vars)]
156
+ angles += angles[:1]
157
+
158
+ # Extender valores para cerrar polígonos
159
+ values_user += values_user[:1]
160
+ values_pattern += values_pattern[:1]
161
+
162
+ # Configurar ejes y etiquetas
163
+ ax.set_xticks(angles[:-1])
164
+ ax.set_xticklabels(categories, fontsize=8)
165
+
166
+ # Círculos concéntricos y etiquetas
167
+ circle_ticks = np.arange(0, 1.1, 0.2) # Reducido a 5 niveles
168
+ ax.set_yticks(circle_ticks)
169
+ ax.set_yticklabels([f'{tick:.1f}' for tick in circle_ticks], fontsize=8)
170
+ ax.set_ylim(0, 1)
171
+
172
+ # Dibujar patrón ideal
173
+ ax.plot(angles, values_pattern, 'g--', linewidth=1, label='Patrón', alpha=0.5)
174
+ ax.fill(angles, values_pattern, 'g', alpha=0.1)
175
+
176
+ # Dibujar valores del usuario
177
+ ax.plot(angles, values_user, 'b-', linewidth=2, label='Tu escritura')
178
+ ax.fill(angles, values_user, 'b', alpha=0.2)
179
+
180
+ # Leyenda
181
+ ax.legend(loc='upper right', bbox_to_anchor=(0.1, 0.1), fontsize=8)
182
+
183
+ # Ajustes finales
184
+ plt.tight_layout()
185
+ st.pyplot(fig)
186
+ plt.close()
187
+
188
+ except Exception as e:
189
+ logger.error(f"Error generando gráfico de radar: {str(e)}")
190
  st.error("Error al generar la visualización")
modules/studentact/current_situation_interface.py CHANGED
@@ -1,397 +1,321 @@
1
- # modules/studentact/current_situation_interface.py
2
-
3
- import streamlit as st
4
- import logging
5
- from ..utils.widget_utils import generate_unique_key
6
- import matplotlib.pyplot as plt
7
- import numpy as np
8
- from ..database.current_situation_mongo_db import store_current_situation_result
9
-
10
- # Importaciones locales
11
- from translations import get_translations
12
-
13
- # Importamos la función de recomendaciones personalizadas si existe
14
- try:
15
- from .claude_recommendations import display_personalized_recommendations
16
- except ImportError:
17
- # Si no existe el módulo, definimos una función placeholder
18
- def display_personalized_recommendations(text, metrics, text_type, lang_code, t):
19
- st.warning("Módulo de recomendaciones personalizadas no disponible. Por favor, contacte al administrador.")
20
-
21
- from .current_situation_analysis import (
22
- analyze_text_dimensions,
23
- analyze_clarity,
24
- analyze_vocabulary_diversity,
25
- analyze_cohesion,
26
- analyze_structure,
27
- get_dependency_depths,
28
- normalize_score,
29
- generate_sentence_graphs,
30
- generate_word_connections,
31
- generate_connection_paths,
32
- create_vocabulary_network,
33
- create_syntax_complexity_graph,
34
- create_cohesion_heatmap
35
- )
36
-
37
- # Configuración del estilo de matplotlib para el gráfico de radar
38
- plt.rcParams['font.family'] = 'sans-serif'
39
- plt.rcParams['axes.grid'] = True
40
- plt.rcParams['axes.spines.top'] = False
41
- plt.rcParams['axes.spines.right'] = False
42
-
43
- logger = logging.getLogger(__name__)
44
-
45
- ####################################
46
-
47
- TEXT_TYPES = {
48
- 'academic_article': {
49
- 'name': 'Artículo Académico',
50
- 'thresholds': {
51
- 'vocabulary': {'min': 0.70, 'target': 0.85},
52
- 'structure': {'min': 0.75, 'target': 0.90},
53
- 'cohesion': {'min': 0.65, 'target': 0.80},
54
- 'clarity': {'min': 0.70, 'target': 0.85}
55
- }
56
- },
57
- 'student_essay': {
58
- 'name': 'Trabajo Universitario',
59
- 'thresholds': {
60
- 'vocabulary': {'min': 0.60, 'target': 0.75},
61
- 'structure': {'min': 0.65, 'target': 0.80},
62
- 'cohesion': {'min': 0.55, 'target': 0.70},
63
- 'clarity': {'min': 0.60, 'target': 0.75}
64
- }
65
- },
66
- 'general_communication': {
67
- 'name': 'Comunicación General',
68
- 'thresholds': {
69
- 'vocabulary': {'min': 0.50, 'target': 0.65},
70
- 'structure': {'min': 0.55, 'target': 0.70},
71
- 'cohesion': {'min': 0.45, 'target': 0.60},
72
- 'clarity': {'min': 0.50, 'target': 0.65}
73
- }
74
- }
75
- }
76
- ####################################
77
-
78
- def display_current_situation_interface(lang_code, nlp_models, t):
79
- """
80
- Interfaz simplificada con gráfico de radar para visualizar métricas.
81
- """
82
- # Inicializar estados si no existen
83
- if 'text_input' not in st.session_state:
84
- st.session_state.text_input = ""
85
- if 'text_area' not in st.session_state: # Añadir inicialización de text_area
86
- st.session_state.text_area = ""
87
- if 'show_results' not in st.session_state:
88
- st.session_state.show_results = False
89
- if 'current_doc' not in st.session_state:
90
- st.session_state.current_doc = None
91
- if 'current_metrics' not in st.session_state:
92
- st.session_state.current_metrics = None
93
-
94
- try:
95
- # Container principal con dos columnas
96
- with st.container():
97
- input_col, results_col = st.columns([1,2])
98
-
99
- with input_col:
100
- # Text area con manejo de estado
101
- text_input = st.text_area(
102
- t.get('input_prompt', "Escribe o pega tu texto aquí:"),
103
- height=400,
104
- key="text_area",
105
- value=st.session_state.text_input,
106
- help="Este texto será analizado para darte recomendaciones personalizadas"
107
- )
108
-
109
- # Función para manejar cambios de texto
110
- if text_input != st.session_state.text_input:
111
- st.session_state.text_input = text_input
112
- st.session_state.show_results = False
113
-
114
- if st.button(
115
- t.get('analyze_button', "Analizar mi escritura"),
116
- type="primary",
117
- disabled=not text_input.strip(),
118
- use_container_width=True,
119
- ):
120
- try:
121
- with st.spinner(t.get('processing', "Analizando...")):
122
- doc = nlp_models[lang_code](text_input)
123
- metrics = analyze_text_dimensions(doc)
124
-
125
- storage_success = store_current_situation_result(
126
- username=st.session_state.username,
127
- text=text_input,
128
- metrics=metrics,
129
- feedback=None
130
- )
131
-
132
- if not storage_success:
133
- logger.warning("No se pudo guardar el análisis en la base de datos")
134
-
135
- st.session_state.current_doc = doc
136
- st.session_state.current_metrics = metrics
137
- st.session_state.show_results = True
138
-
139
- except Exception as e:
140
- logger.error(f"Error en análisis: {str(e)}")
141
- st.error(t.get('analysis_error', "Error al analizar el texto"))
142
-
143
- # Mostrar resultados en la columna derecha
144
- with results_col:
145
- if st.session_state.show_results and st.session_state.current_metrics is not None:
146
- # Primero los radio buttons para tipo de texto
147
- st.markdown("### Tipo de texto")
148
- text_type = st.radio(
149
- "",
150
- options=list(TEXT_TYPES.keys()),
151
- format_func=lambda x: TEXT_TYPES[x]['name'],
152
- horizontal=True,
153
- key="text_type_radio",
154
- help="Selecciona el tipo de texto para ajustar los criterios de evaluación"
155
- )
156
-
157
- st.session_state.current_text_type = text_type
158
-
159
- # Luego mostrar los resultados
160
- display_results(
161
- metrics=st.session_state.current_metrics,
162
- text_type=text_type
163
- )
164
-
165
- except Exception as e:
166
- logger.error(f"Error en interfaz principal: {str(e)}")
167
- st.error("Ocurrió un error al cargar la interfaz")
168
-
169
- ###################################3333
170
-
171
- def display_results(metrics, text_type=None):
172
- """
173
- Muestra los resultados del análisis: métricas verticalmente y gráfico radar.
174
- """
175
- try:
176
- # Usar valor por defecto si no se especifica tipo
177
- text_type = text_type or 'student_essay'
178
-
179
- # Obtener umbrales según el tipo de texto
180
- thresholds = TEXT_TYPES[text_type]['thresholds']
181
-
182
- # Crear dos columnas para las métricas y el gráfico
183
- metrics_col, graph_col = st.columns([1, 1.5])
184
-
185
- # Columna de métricas
186
- with metrics_col:
187
- metrics_config = [
188
- {
189
- 'label': "Vocabulario",
190
- 'key': 'vocabulary',
191
- 'value': metrics['vocabulary']['normalized_score'],
192
- 'help': "Riqueza y variedad del vocabulario",
193
- 'thresholds': thresholds['vocabulary']
194
- },
195
- {
196
- 'label': "Estructura",
197
- 'key': 'structure',
198
- 'value': metrics['structure']['normalized_score'],
199
- 'help': "Organización y complejidad de oraciones",
200
- 'thresholds': thresholds['structure']
201
- },
202
- {
203
- 'label': "Cohesión",
204
- 'key': 'cohesion',
205
- 'value': metrics['cohesion']['normalized_score'],
206
- 'help': "Conexión y fluidez entre ideas",
207
- 'thresholds': thresholds['cohesion']
208
- },
209
- {
210
- 'label': "Claridad",
211
- 'key': 'clarity',
212
- 'value': metrics['clarity']['normalized_score'],
213
- 'help': "Facilidad de comprensión del texto",
214
- 'thresholds': thresholds['clarity']
215
- }
216
- ]
217
-
218
- # Mostrar métricas
219
- for metric in metrics_config:
220
- value = metric['value']
221
- if value < metric['thresholds']['min']:
222
- status = "⚠️ Por mejorar"
223
- color = "inverse"
224
- elif value < metric['thresholds']['target']:
225
- status = "📈 Aceptable"
226
- color = "off"
227
- else:
228
- status = "✅ Óptimo"
229
- color = "normal"
230
-
231
- st.metric(
232
- metric['label'],
233
- f"{value:.2f}",
234
- f"{status} (Meta: {metric['thresholds']['target']:.2f})",
235
- delta_color=color,
236
- help=metric['help']
237
- )
238
- st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
239
-
240
- # Gráfico radar en la columna derecha
241
- with graph_col:
242
- display_radar_chart(metrics_config, thresholds)
243
-
244
- except Exception as e:
245
- logger.error(f"Error mostrando resultados: {str(e)}")
246
- st.error("Error al mostrar los resultados")
247
-
248
-
249
- ######################################
250
- def display_radar_chart(metrics_config, thresholds):
251
- """
252
- Muestra el gráfico radar con los resultados.
253
- """
254
- try:
255
- # Preparar datos para el gráfico
256
- categories = [m['label'] for m in metrics_config]
257
- values_user = [m['value'] for m in metrics_config]
258
- min_values = [m['thresholds']['min'] for m in metrics_config]
259
- target_values = [m['thresholds']['target'] for m in metrics_config]
260
-
261
- # Crear y configurar gráfico
262
- fig = plt.figure(figsize=(8, 8))
263
- ax = fig.add_subplot(111, projection='polar')
264
-
265
- # Configurar radar
266
- angles = [n / float(len(categories)) * 2 * np.pi for n in range(len(categories))]
267
- angles += angles[:1]
268
- values_user += values_user[:1]
269
- min_values += min_values[:1]
270
- target_values += target_values[:1]
271
-
272
- # Configurar ejes
273
- ax.set_xticks(angles[:-1])
274
- ax.set_xticklabels(categories, fontsize=10)
275
- circle_ticks = np.arange(0, 1.1, 0.2)
276
- ax.set_yticks(circle_ticks)
277
- ax.set_yticklabels([f'{tick:.1f}' for tick in circle_ticks], fontsize=8)
278
- ax.set_ylim(0, 1)
279
-
280
- # Dibujar áreas de umbrales
281
- ax.plot(angles, min_values, '#e74c3c', linestyle='--', linewidth=1, label='Mínimo', alpha=0.5)
282
- ax.plot(angles, target_values, '#2ecc71', linestyle='--', linewidth=1, label='Meta', alpha=0.5)
283
- ax.fill_between(angles, target_values, [1]*len(angles), color='#2ecc71', alpha=0.1)
284
- ax.fill_between(angles, [0]*len(angles), min_values, color='#e74c3c', alpha=0.1)
285
-
286
- # Dibujar valores del usuario
287
- ax.plot(angles, values_user, '#3498db', linewidth=2, label='Tu escritura')
288
- ax.fill(angles, values_user, '#3498db', alpha=0.2)
289
-
290
- # Ajustar leyenda
291
- ax.legend(
292
- loc='upper right',
293
- bbox_to_anchor=(1.3, 1.1), # Cambiado de (0.1, 0.1) a (1.3, 1.1)
294
- fontsize=10,
295
- frameon=True,
296
- facecolor='white',
297
- edgecolor='none',
298
- shadow=True
299
- )
300
-
301
- plt.tight_layout()
302
- st.pyplot(fig)
303
- plt.close()
304
-
305
- except Exception as e:
306
- logger.error(f"Error mostrando gráfico radar: {str(e)}")
307
- st.error("Error al mostrar el gráfico")
308
- #######################################
309
-
310
- def display_recommendations(recommendations, t):
311
- """
312
- Muestra las recomendaciones con un diseño de tarjetas.
313
- """
314
- # Definir colores para cada categoría
315
- colors = {
316
- 'vocabulary': '#2E86C1', # Azul
317
- 'structure': '#28B463', # Verde
318
- 'cohesion': '#F39C12', # Naranja
319
- 'clarity': '#9B59B6', # Púrpura
320
- 'priority': '#E74C3C' # Rojo para la categoría prioritaria
321
- }
322
-
323
- # Iconos para cada categoría
324
- icons = {
325
- 'vocabulary': '📚',
326
- 'structure': '🏗️',
327
- 'cohesion': '🔄',
328
- 'clarity': '💡',
329
- 'priority': '⭐'
330
- }
331
-
332
- # Obtener traducciones para cada dimensión
333
- dimension_names = {
334
- 'vocabulary': t.get('SITUATION_ANALYSIS', {}).get('vocabulary', "Vocabulario"),
335
- 'structure': t.get('SITUATION_ANALYSIS', {}).get('structure', "Estructura"),
336
- 'cohesion': t.get('SITUATION_ANALYSIS', {}).get('cohesion', "Cohesión"),
337
- 'clarity': t.get('SITUATION_ANALYSIS', {}).get('clarity', "Claridad"),
338
- 'priority': t.get('SITUATION_ANALYSIS', {}).get('priority', "Prioridad")
339
- }
340
-
341
- # Título de la sección prioritaria
342
- priority_focus = t.get('SITUATION_ANALYSIS', {}).get('priority_focus', 'Área prioritaria para mejorar')
343
- st.markdown(f"### {icons['priority']} {priority_focus}")
344
-
345
- # Determinar área prioritaria (la que tiene menor puntuación)
346
- priority_area = recommendations.get('priority', 'vocabulary')
347
- priority_title = dimension_names.get(priority_area, "Área prioritaria")
348
-
349
- # Determinar el contenido para mostrar
350
- if isinstance(recommendations[priority_area], dict) and 'title' in recommendations[priority_area]:
351
- priority_title = recommendations[priority_area]['title']
352
- priority_content = recommendations[priority_area]['content']
353
- else:
354
- priority_content = recommendations[priority_area]
355
-
356
- # Mostrar la recomendación prioritaria con un estilo destacado
357
- with st.container():
358
- st.markdown(
359
- f"""
360
- <div style="border:2px solid {colors['priority']}; border-radius:5px; padding:15px; margin-bottom:20px;">
361
- <h4 style="color:{colors['priority']};">{priority_title}</h4>
362
- <p>{priority_content}</p>
363
- </div>
364
- """,
365
- unsafe_allow_html=True
366
- )
367
-
368
- # Crear dos columnas para las tarjetas de recomendaciones restantes
369
- col1, col2 = st.columns(2)
370
-
371
- # Distribuir las recomendaciones en las columnas
372
- categories = ['vocabulary', 'structure', 'cohesion', 'clarity']
373
- for i, category in enumerate(categories):
374
- # Saltar si esta categoría ya es la prioritaria
375
- if category == priority_area:
376
- continue
377
-
378
- # Determinar título y contenido
379
- if isinstance(recommendations[category], dict) and 'title' in recommendations[category]:
380
- category_title = recommendations[category]['title']
381
- category_content = recommendations[category]['content']
382
- else:
383
- category_title = dimension_names.get(category, category)
384
- category_content = recommendations[category]
385
-
386
- # Alternar entre columnas
387
- with col1 if i % 2 == 0 else col2:
388
- # Crear tarjeta para cada recomendación
389
- st.markdown(
390
- f"""
391
- <div style="border:1px solid {colors[category]}; border-radius:5px; padding:10px; margin-bottom:15px;">
392
- <h4 style="color:{colors[category]};">{icons[category]} {category_title}</h4>
393
- <p>{category_content}</p>
394
- </div>
395
- """,
396
- unsafe_allow_html=True
397
- )
 
1
+ # modules/studentact/current_situation_interface.py
2
+
3
+ import streamlit as st
4
+ import logging
5
+ from ..utils.widget_utils import generate_unique_key
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ from ..database.current_situation_mongo_db import store_current_situation_result
9
+
10
+ # Importaciones locales
11
+ from translations import get_translations
12
+
13
+ # Importamos la función de recomendaciones personalizadas si existe
14
+ try:
15
+ from .claude_recommendations import display_personalized_recommendations
16
+ except ImportError:
17
+ # Si no existe el módulo, definimos una función placeholder
18
+ def display_personalized_recommendations(text, metrics, text_type, lang_code, t):
19
+ st.warning("Módulo de recomendaciones personalizadas no disponible. Por favor, contacte al administrador.")
20
+
21
+ from .current_situation_analysis import (
22
+ analyze_text_dimensions,
23
+ analyze_clarity,
24
+ analyze_vocabulary_diversity,
25
+ analyze_cohesion,
26
+ analyze_structure,
27
+ get_dependency_depths,
28
+ normalize_score,
29
+ generate_sentence_graphs,
30
+ generate_word_connections,
31
+ generate_connection_paths,
32
+ create_vocabulary_network,
33
+ create_syntax_complexity_graph,
34
+ create_cohesion_heatmap
35
+ )
36
+
37
+ # Configuración del estilo de matplotlib para el gráfico de radar
38
+ plt.rcParams['font.family'] = 'sans-serif'
39
+ plt.rcParams['axes.grid'] = True
40
+ plt.rcParams['axes.spines.top'] = False
41
+ plt.rcParams['axes.spines.right'] = False
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ ####################################
46
+ # Definición global de los tipos de texto y sus umbrales
47
+ TEXT_TYPES = {
48
+ 'academic_article': {
49
+ 'name': 'Artículo Académico',
50
+ 'thresholds': {
51
+ 'vocabulary': {'min': 0.70, 'target': 0.85},
52
+ 'structure': {'min': 0.75, 'target': 0.90},
53
+ 'cohesion': {'min': 0.65, 'target': 0.80},
54
+ 'clarity': {'min': 0.70, 'target': 0.85}
55
+ }
56
+ },
57
+ 'student_essay': {
58
+ 'name': 'Trabajo Universitario',
59
+ 'thresholds': {
60
+ 'vocabulary': {'min': 0.60, 'target': 0.75},
61
+ 'structure': {'min': 0.65, 'target': 0.80},
62
+ 'cohesion': {'min': 0.55, 'target': 0.70},
63
+ 'clarity': {'min': 0.60, 'target': 0.75}
64
+ }
65
+ },
66
+ 'general_communication': {
67
+ 'name': 'Comunicación General',
68
+ 'thresholds': {
69
+ 'vocabulary': {'min': 0.50, 'target': 0.65},
70
+ 'structure': {'min': 0.55, 'target': 0.70},
71
+ 'cohesion': {'min': 0.45, 'target': 0.60},
72
+ 'clarity': {'min': 0.50, 'target': 0.65}
73
+ }
74
+ }
75
+ }
76
+ ####################################
77
+
78
+ def display_current_situation_interface(lang_code, nlp_models, t):
79
+ """
80
+ Interfaz simplificada con gráfico de radar para visualizar métricas.
81
+ """
82
+ # Inicializar estados si no existen
83
+ if 'text_input' not in st.session_state:
84
+ st.session_state.text_input = ""
85
+ if 'text_area' not in st.session_state: # Añadir inicialización de text_area
86
+ st.session_state.text_area = ""
87
+ if 'show_results' not in st.session_state:
88
+ st.session_state.show_results = False
89
+ if 'current_doc' not in st.session_state:
90
+ st.session_state.current_doc = None
91
+ if 'current_metrics' not in st.session_state:
92
+ st.session_state.current_metrics = None
93
+ if 'current_recommendations' not in st.session_state:
94
+ st.session_state.current_recommendations = None
95
+
96
+ try:
97
+ # Container principal con dos columnas
98
+ with st.container():
99
+ input_col, results_col = st.columns([1,2])
100
+
101
+ with input_col:
102
+ # Text area con manejo de estado
103
+ text_input = st.text_area(
104
+ t.get('input_prompt', "Escribe o pega tu texto aquí:"),
105
+ height=400,
106
+ key="text_area",
107
+ value=st.session_state.text_input,
108
+ help="Este texto será analizado para darte recomendaciones personalizadas"
109
+ )
110
+
111
+ # Función para manejar cambios de texto
112
+ if text_input != st.session_state.text_input:
113
+ st.session_state.text_input = text_input
114
+ st.session_state.show_results = False
115
+
116
+ if st.button(
117
+ t.get('analyze_button', "Analizar mi escritura"),
118
+ type="primary",
119
+ disabled=not text_input.strip(),
120
+ use_container_width=True,
121
+ ):
122
+ try:
123
+ with st.spinner(t.get('processing', "Analizando...")):
124
+ doc = nlp_models[lang_code](text_input)
125
+ metrics = analyze_text_dimensions(doc)
126
+
127
+ storage_success = store_current_situation_result(
128
+ username=st.session_state.username,
129
+ text=text_input,
130
+ metrics=metrics,
131
+ feedback=None
132
+ )
133
+
134
+ if not storage_success:
135
+ logger.warning("No se pudo guardar el análisis en la base de datos")
136
+
137
+ st.session_state.current_doc = doc
138
+ st.session_state.current_metrics = metrics
139
+ st.session_state.show_results = True
140
+
141
+ except Exception as e:
142
+ logger.error(f"Error en análisis: {str(e)}")
143
+ st.error(t.get('analysis_error', "Error al analizar el texto"))
144
+
145
+ # Mostrar resultados en la columna derecha
146
+ with results_col:
147
+ if st.session_state.show_results and st.session_state.current_metrics is not None:
148
+ # Primero los radio buttons para tipo de texto
149
+ st.markdown("### Tipo de texto")
150
+ text_type = st.radio(
151
+ label="Tipo de texto",
152
+ options=list(TEXT_TYPES.keys()),
153
+ format_func=lambda x: TEXT_TYPES[x]['name'],
154
+ horizontal=True,
155
+ key="text_type_radio",
156
+ label_visibility="collapsed",
157
+ help="Selecciona el tipo de texto para ajustar los criterios de evaluación"
158
+ )
159
+
160
+ st.session_state.current_text_type = text_type
161
+
162
+ # Crear subtabs
163
+ subtab1, subtab2 = st.tabs(["Diagnóstico", "Recomendaciones"])
164
+
165
+ # Mostrar resultados en el primer subtab
166
+ with subtab1:
167
+ display_diagnosis(
168
+ metrics=st.session_state.current_metrics,
169
+ text_type=text_type
170
+ )
171
+
172
+ # Mostrar recomendaciones en el segundo subtab
173
+ with subtab2:
174
+ # Llamar directamente a la función de recomendaciones personalizadas
175
+ display_personalized_recommendations(
176
+ text=text_input,
177
+ metrics=st.session_state.current_metrics,
178
+ text_type=text_type,
179
+ lang_code=lang_code,
180
+ t=t
181
+ )
182
+
183
+ except Exception as e:
184
+ logger.error(f"Error en interfaz principal: {str(e)}")
185
+ st.error("Ocurrió un error al cargar la interfaz")
186
+
187
+ def display_diagnosis(metrics, text_type=None):
188
+ """
189
+ Muestra los resultados del análisis: métricas verticalmente y gráfico radar.
190
+ """
191
+ try:
192
+ # Usar valor por defecto si no se especifica tipo
193
+ text_type = text_type or 'student_essay'
194
+
195
+ # Obtener umbrales según el tipo de texto
196
+ thresholds = TEXT_TYPES[text_type]['thresholds']
197
+
198
+ # Crear dos columnas para las métricas y el gráfico
199
+ metrics_col, graph_col = st.columns([1, 1.5])
200
+
201
+ # Columna de métricas
202
+ with metrics_col:
203
+ metrics_config = [
204
+ {
205
+ 'label': "Vocabulario",
206
+ 'key': 'vocabulary',
207
+ 'value': metrics['vocabulary']['normalized_score'],
208
+ 'help': "Riqueza y variedad del vocabulario",
209
+ 'thresholds': thresholds['vocabulary']
210
+ },
211
+ {
212
+ 'label': "Estructura",
213
+ 'key': 'structure',
214
+ 'value': metrics['structure']['normalized_score'],
215
+ 'help': "Organización y complejidad de oraciones",
216
+ 'thresholds': thresholds['structure']
217
+ },
218
+ {
219
+ 'label': "Cohesión",
220
+ 'key': 'cohesion',
221
+ 'value': metrics['cohesion']['normalized_score'],
222
+ 'help': "Conexión y fluidez entre ideas",
223
+ 'thresholds': thresholds['cohesion']
224
+ },
225
+ {
226
+ 'label': "Claridad",
227
+ 'key': 'clarity',
228
+ 'value': metrics['clarity']['normalized_score'],
229
+ 'help': "Facilidad de comprensión del texto",
230
+ 'thresholds': thresholds['clarity']
231
+ }
232
+ ]
233
+
234
+ # Mostrar métricas
235
+ for metric in metrics_config:
236
+ value = metric['value']
237
+ if value < metric['thresholds']['min']:
238
+ status = "⚠️ Por mejorar"
239
+ color = "inverse"
240
+ elif value < metric['thresholds']['target']:
241
+ status = "📈 Aceptable"
242
+ color = "off"
243
+ else:
244
+ status = "✅ Óptimo"
245
+ color = "normal"
246
+
247
+ st.metric(
248
+ metric['label'],
249
+ f"{value:.2f}",
250
+ f"{status} (Meta: {metric['thresholds']['target']:.2f})",
251
+ delta_color=color,
252
+ help=metric['help']
253
+ )
254
+ st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
255
+
256
+ # Gráfico radar en la columna derecha
257
+ with graph_col:
258
+ display_radar_chart(metrics_config, thresholds)
259
+
260
+ except Exception as e:
261
+ logger.error(f"Error mostrando resultados: {str(e)}")
262
+ st.error("Error al mostrar los resultados")
263
+
264
+ def display_radar_chart(metrics_config, thresholds):
265
+ """
266
+ Muestra el gráfico radar con los resultados.
267
+ """
268
+ try:
269
+ # Preparar datos para el gráfico
270
+ categories = [m['label'] for m in metrics_config]
271
+ values_user = [m['value'] for m in metrics_config]
272
+ min_values = [m['thresholds']['min'] for m in metrics_config]
273
+ target_values = [m['thresholds']['target'] for m in metrics_config]
274
+
275
+ # Crear y configurar gráfico
276
+ fig = plt.figure(figsize=(8, 8))
277
+ ax = fig.add_subplot(111, projection='polar')
278
+
279
+ # Configurar radar
280
+ angles = [n / float(len(categories)) * 2 * np.pi for n in range(len(categories))]
281
+ angles += angles[:1]
282
+ values_user += values_user[:1]
283
+ min_values += min_values[:1]
284
+ target_values += target_values[:1]
285
+
286
+ # Configurar ejes
287
+ ax.set_xticks(angles[:-1])
288
+ ax.set_xticklabels(categories, fontsize=10)
289
+ circle_ticks = np.arange(0, 1.1, 0.2)
290
+ ax.set_yticks(circle_ticks)
291
+ ax.set_yticklabels([f'{tick:.1f}' for tick in circle_ticks], fontsize=8)
292
+ ax.set_ylim(0, 1)
293
+
294
+ # Dibujar áreas de umbrales
295
+ ax.plot(angles, min_values, '#e74c3c', linestyle='--', linewidth=1, label='Mínimo', alpha=0.5)
296
+ ax.plot(angles, target_values, '#2ecc71', linestyle='--', linewidth=1, label='Meta', alpha=0.5)
297
+ ax.fill_between(angles, target_values, [1]*len(angles), color='#2ecc71', alpha=0.1)
298
+ ax.fill_between(angles, [0]*len(angles), min_values, color='#e74c3c', alpha=0.1)
299
+
300
+ # Dibujar valores del usuario
301
+ ax.plot(angles, values_user, '#3498db', linewidth=2, label='Tu escritura')
302
+ ax.fill(angles, values_user, '#3498db', alpha=0.2)
303
+
304
+ # Ajustar leyenda
305
+ ax.legend(
306
+ loc='upper right',
307
+ bbox_to_anchor=(1.3, 1.1),
308
+ fontsize=10,
309
+ frameon=True,
310
+ facecolor='white',
311
+ edgecolor='none',
312
+ shadow=True
313
+ )
314
+
315
+ plt.tight_layout()
316
+ st.pyplot(fig)
317
+ plt.close()
318
+
319
+ except Exception as e:
320
+ logger.error(f"Error mostrando gráfico radar: {str(e)}")
321
+ st.error("Error al mostrar el gráfico")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/studentact/student_activities.py CHANGED
@@ -1,111 +1,111 @@
1
- #modules/studentact/student_activities.py
2
-
3
- import streamlit as st
4
- import pandas as pd
5
- import matplotlib.pyplot as plt
6
- import seaborn as sns
7
- import base64
8
- from io import BytesIO
9
- from reportlab.pdfgen import canvas
10
- from reportlab.lib.pagesizes import letter
11
- from docx import Document
12
- from odf.opendocument import OpenDocumentText
13
- from odf.text import P
14
- from datetime import datetime, timedelta
15
- import pytz
16
- import logging
17
-
18
- # Configuración de logging
19
- logging.basicConfig(level=logging.DEBUG)
20
- logger = logging.getLogger(__name__)
21
-
22
- # Importaciones locales
23
- try:
24
- from ..database.morphosintax_mongo_db import get_student_morphosyntax_data
25
- from ..database.semantic_mongo_db import get_student_semantic_data
26
- from ..database.discourse_mongo_db import get_student_discourse_data
27
-
28
- from ..database.chat_mongo_db import get_chat_history
29
-
30
- logger.info("Importaciones locales exitosas")
31
- except ImportError as e:
32
- logger.error(f"Error en las importaciones locales: {e}")
33
-
34
- def display_student_progress(username, lang_code, t):
35
- logger.debug(f"Iniciando display_student_progress para {username}")
36
-
37
- st.title(f"{t.get('progress_of', 'Progreso de')} {username}")
38
-
39
- # Obtener los datos del estudiante
40
- student_data = get_student_morphosyntax_data(username)
41
-
42
- if not student_data or len(student_data.get('entries', [])) == 0:
43
- logger.warning(f"No se encontraron datos para el estudiante {username}")
44
- st.warning(t.get("no_data_warning", "No se encontraron datos para este estudiante."))
45
- st.info(t.get("try_analysis", "Intenta realizar algunos análisis de texto primero."))
46
- return
47
-
48
- logger.debug(f"Datos del estudiante obtenidos: {len(student_data['entries'])} entradas")
49
-
50
- # Resumen de actividades
51
- with st.expander(t.get("activities_summary", "Resumen de Actividades"), expanded=True):
52
- total_entries = len(student_data['entries'])
53
- st.write(f"{t.get('total_analyses', 'Total de análisis realizados')}: {total_entries}")
54
-
55
- # Gráfico de tipos de análisis
56
- try:
57
- analysis_types = [entry.get('analysis_type', 'unknown') for entry in student_data['entries']]
58
- analysis_counts = pd.Series(analysis_types).value_counts()
59
- fig, ax = plt.subplots()
60
- sns.barplot(x=analysis_counts.index, y=analysis_counts.values, ax=ax)
61
- ax.set_title(t.get("analysis_types_chart", "Tipos de análisis realizados"))
62
- ax.set_xlabel(t.get("analysis_type", "Tipo de análisis"))
63
- ax.set_ylabel(t.get("count", "Cantidad"))
64
- st.pyplot(fig)
65
- except Exception as e:
66
- logger.error(f"Error al crear el gráfico: {e}")
67
- st.error("No se pudo crear el gráfico de tipos de análisis.")
68
-
69
- # Función para generar el contenido del archivo de actividades de las últimas 48 horas
70
- def generate_activity_content_48h():
71
- content = f"Actividades de {username} en las últimas 48 horas\n\n"
72
-
73
- two_days_ago = datetime.now(pytz.utc) - timedelta(days=2)
74
-
75
- try:
76
- morphosyntax_analyses = get_student_morphosyntax_data(username)
77
- recent_morphosyntax = [a for a in morphosyntax_analyses if datetime.fromisoformat(a['timestamp']) > two_days_ago]
78
-
79
- content += f"Análisis morfosintácticos: {len(recent_morphosyntax)}\n"
80
- for analysis in recent_morphosyntax:
81
- content += f"- Análisis del {analysis['timestamp']}: {analysis['text'][:50]}...\n"
82
-
83
- chat_history = get_chat_history(username, None)
84
- recent_chats = [c for c in chat_history if datetime.fromisoformat(c['timestamp']) > two_days_ago]
85
-
86
- content += f"\nConversaciones de chat: {len(recent_chats)}\n"
87
- for chat in recent_chats:
88
- content += f"- Chat del {chat['timestamp']}: {len(chat['messages'])} mensajes\n"
89
- except Exception as e:
90
- logger.error(f"Error al generar el contenido de actividades: {e}")
91
- content += "Error al recuperar los datos de actividades.\n"
92
-
93
- return content
94
-
95
- # Botones para descargar el histórico de actividades de las últimas 48 horas
96
- st.subheader(t.get("download_history_48h", "Descargar Histórico de Actividades (Últimas 48 horas)"))
97
- if st.button("Generar reporte de 48 horas"):
98
- try:
99
- report_content = generate_activity_content_48h()
100
- st.text_area("Reporte de 48 horas", report_content, height=300)
101
- st.download_button(
102
- label="Descargar TXT (48h)",
103
- data=report_content,
104
- file_name="actividades_48h.txt",
105
- mime="text/plain"
106
- )
107
- except Exception as e:
108
- logger.error(f"Error al generar el reporte: {e}")
109
- st.error("No se pudo generar el reporte. Por favor, verifica los logs para más detalles.")
110
-
111
  logger.debug("Finalizando display_student_progress")
 
1
+ #modules/studentact/student_activities.py
2
+
3
+ import streamlit as st
4
+ import pandas as pd
5
+ import matplotlib.pyplot as plt
6
+ import seaborn as sns
7
+ import base64
8
+ from io import BytesIO
9
+ from reportlab.pdfgen import canvas
10
+ from reportlab.lib.pagesizes import letter
11
+ from docx import Document
12
+ from odf.opendocument import OpenDocumentText
13
+ from odf.text import P
14
+ from datetime import datetime, timedelta
15
+ import pytz
16
+ import logging
17
+
18
+ # Configuración de logging
19
+ logging.basicConfig(level=logging.DEBUG)
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Importaciones locales
23
+ try:
24
+ from ..database.morphosintax_mongo_db import get_student_morphosyntax_data
25
+ from ..database.semantic_mongo_db import get_student_semantic_data
26
+ from ..database.discourse_mongo_db import get_student_discourse_data
27
+
28
+ from ..database.chat_mongo_db import get_chat_history
29
+
30
+ logger.info("Importaciones locales exitosas")
31
+ except ImportError as e:
32
+ logger.error(f"Error en las importaciones locales: {e}")
33
+
34
+ def display_student_progress(username, lang_code, t):
35
+ logger.debug(f"Iniciando display_student_progress para {username}")
36
+
37
+ st.title(f"{t.get('progress_of', 'Progreso de')} {username}")
38
+
39
+ # Obtener los datos del estudiante
40
+ student_data = get_student_morphosyntax_data(username)
41
+
42
+ if not student_data or len(student_data.get('entries', [])) == 0:
43
+ logger.warning(f"No se encontraron datos para el estudiante {username}")
44
+ st.warning(t.get("no_data_warning", "No se encontraron datos para este estudiante."))
45
+ st.info(t.get("try_analysis", "Intenta realizar algunos análisis de texto primero."))
46
+ return
47
+
48
+ logger.debug(f"Datos del estudiante obtenidos: {len(student_data['entries'])} entradas")
49
+
50
+ # Resumen de actividades
51
+ with st.expander(t.get("activities_summary", "Resumen de Actividades"), expanded=True):
52
+ total_entries = len(student_data['entries'])
53
+ st.write(f"{t.get('total_analyses', 'Total de análisis realizados')}: {total_entries}")
54
+
55
+ # Gráfico de tipos de análisis
56
+ try:
57
+ analysis_types = [entry.get('analysis_type', 'unknown') for entry in student_data['entries']]
58
+ analysis_counts = pd.Series(analysis_types).value_counts()
59
+ fig, ax = plt.subplots()
60
+ sns.barplot(x=analysis_counts.index, y=analysis_counts.values, ax=ax)
61
+ ax.set_title(t.get("analysis_types_chart", "Tipos de análisis realizados"))
62
+ ax.set_xlabel(t.get("analysis_type", "Tipo de análisis"))
63
+ ax.set_ylabel(t.get("count", "Cantidad"))
64
+ st.pyplot(fig)
65
+ except Exception as e:
66
+ logger.error(f"Error al crear el gráfico: {e}")
67
+ st.error("No se pudo crear el gráfico de tipos de análisis.")
68
+
69
+ # Función para generar el contenido del archivo de actividades de las últimas 48 horas
70
+ def generate_activity_content_48h():
71
+ content = f"Actividades de {username} en las últimas 48 horas\n\n"
72
+
73
+ two_days_ago = datetime.now(pytz.utc) - timedelta(days=2)
74
+
75
+ try:
76
+ morphosyntax_analyses = get_student_morphosyntax_data(username)
77
+ recent_morphosyntax = [a for a in morphosyntax_analyses if datetime.fromisoformat(a['timestamp']) > two_days_ago]
78
+
79
+ content += f"Análisis morfosintácticos: {len(recent_morphosyntax)}\n"
80
+ for analysis in recent_morphosyntax:
81
+ content += f"- Análisis del {analysis['timestamp']}: {analysis['text'][:50]}...\n"
82
+
83
+ chat_history = get_chat_history(username, None)
84
+ recent_chats = [c for c in chat_history if datetime.fromisoformat(c['timestamp']) > two_days_ago]
85
+
86
+ content += f"\nConversaciones de chat: {len(recent_chats)}\n"
87
+ for chat in recent_chats:
88
+ content += f"- Chat del {chat['timestamp']}: {len(chat['messages'])} mensajes\n"
89
+ except Exception as e:
90
+ logger.error(f"Error al generar el contenido de actividades: {e}")
91
+ content += "Error al recuperar los datos de actividades.\n"
92
+
93
+ return content
94
+
95
+ # Botones para descargar el histórico de actividades de las últimas 48 horas
96
+ st.subheader(t.get("download_history_48h", "Descargar Histórico de Actividades (Últimas 48 horas)"))
97
+ if st.button("Generar reporte de 48 horas"):
98
+ try:
99
+ report_content = generate_activity_content_48h()
100
+ st.text_area("Reporte de 48 horas", report_content, height=300)
101
+ st.download_button(
102
+ label="Descargar TXT (48h)",
103
+ data=report_content,
104
+ file_name="actividades_48h.txt",
105
+ mime="text/plain"
106
+ )
107
+ except Exception as e:
108
+ logger.error(f"Error al generar el reporte: {e}")
109
+ st.error("No se pudo generar el reporte. Por favor, verifica los logs para más detalles.")
110
+
111
  logger.debug("Finalizando display_student_progress")
modules/studentact/student_activities_v2-complet.py CHANGED
@@ -1,794 +1,794 @@
1
- ##############
2
- ###modules/studentact/student_activities_v2.py
3
-
4
- import streamlit as st
5
- import re
6
- import io
7
- from io import BytesIO
8
- import pandas as pd
9
- import numpy as np
10
- import time
11
- import matplotlib.pyplot as plt
12
- from datetime import datetime
13
- from spacy import displacy
14
- import random
15
- import base64
16
- import seaborn as sns
17
- import logging
18
-
19
- # Importaciones de la base de datos
20
- from ..database.morphosintax_mongo_db import get_student_morphosyntax_analysis
21
- from ..database.semantic_mongo_db import get_student_semantic_analysis
22
- from ..database.discourse_mongo_db import get_student_discourse_analysis
23
- from ..database.chat_mongo_db import get_chat_history
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
- ###################################################################################
28
-
29
- def display_student_activities(username: str, lang_code: str, t: dict):
30
- """
31
- Muestra todas las actividades del estudiante
32
- Args:
33
- username: Nombre del estudiante
34
- lang_code: Código del idioma
35
- t: Diccionario de traducciones
36
- """
37
- try:
38
- st.header(t.get('activities_title', 'Mis Actividades'))
39
-
40
- # Tabs para diferentes tipos de análisis
41
- tabs = st.tabs([
42
- t.get('morpho_activities', 'Análisis Morfosintáctico'),
43
- t.get('semantic_activities', 'Análisis Semántico'),
44
- t.get('discourse_activities', 'Análisis del Discurso'),
45
- t.get('chat_activities', 'Conversaciones con el Asistente')
46
- ])
47
-
48
- # Tab de Análisis Morfosintáctico
49
- with tabs[0]:
50
- display_morphosyntax_activities(username, t)
51
-
52
- # Tab de Análisis Semántico
53
- with tabs[1]:
54
- display_semantic_activities(username, t)
55
-
56
- # Tab de Análisis del Discurso
57
- with tabs[2]:
58
- display_discourse_activities(username, t)
59
-
60
- # Tab de Conversaciones del Chat
61
- with tabs[3]:
62
- display_chat_activities(username, t)
63
-
64
- except Exception as e:
65
- logger.error(f"Error mostrando actividades: {str(e)}")
66
- st.error(t.get('error_loading_activities', 'Error al cargar las actividades'))
67
-
68
-
69
- ###############################################################################################
70
- def display_morphosyntax_activities(username: str, t: dict):
71
- """Muestra actividades de análisis morfosintáctico"""
72
- try:
73
- analyses = get_student_morphosyntax_analysis(username)
74
- if not analyses:
75
- st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
76
- return
77
-
78
- for analysis in analyses:
79
- with st.expander(
80
- f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
81
- expanded=False
82
- ):
83
- st.text(f"{t.get('analyzed_text', 'Texto analizado')}:")
84
- st.write(analysis['text'])
85
-
86
- if 'arc_diagrams' in analysis:
87
- st.subheader(t.get('syntactic_diagrams', 'Diagramas sintácticos'))
88
- for diagram in analysis['arc_diagrams']:
89
- st.write(diagram, unsafe_allow_html=True)
90
-
91
- except Exception as e:
92
- logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
93
- st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
94
-
95
-
96
- ###############################################################################################
97
- def display_semantic_activities(username: str, t: dict):
98
- """Muestra actividades de análisis semántico"""
99
- try:
100
- logger.info(f"Recuperando análisis semántico para {username}")
101
- analyses = get_student_semantic_analysis(username)
102
-
103
- if not analyses:
104
- logger.info("No se encontraron análisis semánticos")
105
- st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
106
- return
107
-
108
- logger.info(f"Procesando {len(analyses)} análisis semánticos")
109
- for analysis in analyses:
110
- try:
111
- # Verificar campos mínimos necesarios
112
- if not all(key in analysis for key in ['timestamp', 'concept_graph']):
113
- logger.warning(f"Análisis incompleto: {analysis.keys()}")
114
- continue
115
-
116
- # Formatear fecha
117
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
118
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
119
-
120
- with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
121
- if analysis['concept_graph']:
122
- logger.debug("Decodificando gráfico de conceptos")
123
- try:
124
- image_bytes = base64.b64decode(analysis['concept_graph'])
125
- st.image(image_bytes, use_column_width=True)
126
- logger.debug("Gráfico mostrado exitosamente")
127
- except Exception as img_error:
128
- logger.error(f"Error decodificando imagen: {str(img_error)}")
129
- st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
130
- else:
131
- st.info(t.get('no_graph', 'No hay visualización disponible'))
132
-
133
- except Exception as e:
134
- logger.error(f"Error procesando análisis individual: {str(e)}")
135
- continue
136
-
137
- except Exception as e:
138
- logger.error(f"Error mostrando análisis semántico: {str(e)}")
139
- st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
140
-
141
-
142
- ###################################################################################################
143
- def display_discourse_activities(username: str, t: dict):
144
- """Muestra actividades de análisis del discurso"""
145
- try:
146
- logger.info(f"Recuperando análisis del discurso para {username}")
147
- analyses = get_student_discourse_analysis(username)
148
-
149
- if not analyses:
150
- logger.info("No se encontraron análisis del discurso")
151
- st.info(t.get('no_discourse_analyses', 'No hay análisis del discurso registrados'))
152
- return
153
-
154
- logger.info(f"Procesando {len(analyses)} análisis del discurso")
155
- for analysis in analyses:
156
- try:
157
- # Verificar campos mínimos necesarios
158
- if not all(key in analysis for key in ['timestamp', 'combined_graph']):
159
- logger.warning(f"Análisis incompleto: {analysis.keys()}")
160
- continue
161
-
162
- # Formatear fecha
163
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
164
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
165
-
166
- with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
167
- if analysis['combined_graph']:
168
- logger.debug("Decodificando gráfico combinado")
169
- try:
170
- image_bytes = base64.b64decode(analysis['combined_graph'])
171
- st.image(image_bytes, use_column_width=True)
172
- logger.debug("Gráfico mostrado exitosamente")
173
- except Exception as img_error:
174
- logger.error(f"Error decodificando imagen: {str(img_error)}")
175
- st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
176
- else:
177
- st.info(t.get('no_visualization', 'No hay visualización comparativa disponible'))
178
-
179
- except Exception as e:
180
- logger.error(f"Error procesando análisis individual: {str(e)}")
181
- continue
182
-
183
- except Exception as e:
184
- logger.error(f"Error mostrando análisis del discurso: {str(e)}")
185
- st.error(t.get('error_discourse', 'Error al mostrar análisis del discurso'))
186
-
187
- #################################################################################
188
- def display_discourse_comparison(analysis: dict, t: dict):
189
- """Muestra la comparación de análisis del discurso"""
190
- st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
191
-
192
- col1, col2 = st.columns(2)
193
- with col1:
194
- st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
195
- df1 = pd.DataFrame(analysis['key_concepts1'])
196
- st.dataframe(df1)
197
-
198
- with col2:
199
- st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
200
- df2 = pd.DataFrame(analysis['key_concepts2'])
201
- st.dataframe(df2)
202
-
203
- #################################################################################
204
- def display_chat_activities(username: str, t: dict):
205
- """
206
- Muestra historial de conversaciones del chat
207
- """
208
- try:
209
- # Obtener historial del chat
210
- chat_history = get_chat_history(
211
- username=username,
212
- analysis_type='sidebar',
213
- limit=50
214
- )
215
-
216
- if not chat_history:
217
- st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
218
- return
219
-
220
- for chat in reversed(chat_history): # Mostrar las más recientes primero
221
- try:
222
- # Convertir timestamp a datetime para formato
223
- timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
224
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
225
-
226
- with st.expander(
227
- f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
228
- expanded=False
229
- ):
230
- if 'messages' in chat and chat['messages']:
231
- # Mostrar cada mensaje en la conversación
232
- for message in chat['messages']:
233
- role = message.get('role', 'unknown')
234
- content = message.get('content', '')
235
-
236
- # Usar el componente de chat de Streamlit
237
- with st.chat_message(role):
238
- st.markdown(content)
239
-
240
- # Agregar separador entre mensajes
241
- st.divider()
242
- else:
243
- st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
244
-
245
- except Exception as e:
246
- logger.error(f"Error mostrando conversación: {str(e)}")
247
- continue
248
-
249
- except Exception as e:
250
- logger.error(f"Error mostrando historial del chat: {str(e)}")
251
- st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
- '''
262
- ##########versión 25-9-2024---02:30 ################ OK (username)####################
263
-
264
- def display_student_progress(username, lang_code, t, student_data):
265
- st.title(f"{t.get('progress_of', 'Progreso de')} {username}")
266
-
267
- if not student_data or len(student_data.get('entries', [])) == 0:
268
- st.warning(t.get("no_data_warning", "No se encontraron datos para este estudiante."))
269
- st.info(t.get("try_analysis", "Intenta realizar algunos análisis de texto primero."))
270
- return
271
-
272
- with st.expander(t.get("activities_summary", "Resumen de Actividades"), expanded=True):
273
- total_entries = len(student_data['entries'])
274
- st.write(f"{t.get('total_analyses', 'Total de análisis realizados')}: {total_entries}")
275
-
276
- # Gráfico de tipos de análisis
277
- analysis_types = [entry['analysis_type'] for entry in student_data['entries']]
278
- analysis_counts = pd.Series(analysis_types).value_counts()
279
- fig, ax = plt.subplots()
280
- analysis_counts.plot(kind='bar', ax=ax)
281
- ax.set_title(t.get("analysis_types_chart", "Tipos de análisis realizados"))
282
- ax.set_xlabel(t.get("analysis_type", "Tipo de análisis"))
283
- ax.set_ylabel(t.get("count", "Cantidad"))
284
- st.pyplot(fig)
285
-
286
- # Mostrar los últimos análisis morfosintácticos
287
- with st.expander(t.get("morphosyntax_history", "Histórico de Análisis Morfosintácticos")):
288
- morphosyntax_entries = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'morphosyntax']
289
- for entry in morphosyntax_entries[:5]: # Mostrar los últimos 5
290
- st.subheader(f"{t.get('analysis_of', 'Análisis del')} {entry['timestamp']}")
291
- if 'arc_diagrams' in entry and entry['arc_diagrams']:
292
- st.components.v1.html(entry['arc_diagrams'][0], height=300, scrolling=True)
293
-
294
- # Añadir secciones similares para análisis semánticos y discursivos si es necesario
295
-
296
- # Mostrar el historial de chat
297
- with st.expander(t.get("chat_history", "Historial de Chat")):
298
- if 'chat_history' in student_data:
299
- for chat in student_data['chat_history'][:5]: # Mostrar las últimas 5 conversaciones
300
- st.subheader(f"{t.get('chat_from', 'Chat del')} {chat['timestamp']}")
301
- for message in chat['messages']:
302
- st.write(f"{message['role'].capitalize()}: {message['content']}")
303
- st.write("---")
304
- else:
305
- st.write(t.get("no_chat_history", "No hay historial de chat disponible."))
306
-
307
-
308
- ##########versión 24-9-2024---17:30 ################ OK FROM--V2 de def get_student_data(username)####################
309
-
310
- def display_student_progress(username, lang_code, t, student_data):
311
- if not student_data or len(student_data['entries']) == 0:
312
- st.warning(t.get("no_data_warning", "No se encontraron datos para este estudiante."))
313
- st.info(t.get("try_analysis", "Intenta realizar algunos análisis de texto primero."))
314
- return
315
-
316
- st.title(f"{t.get('progress_of', 'Progreso de')} {username}")
317
-
318
- with st.expander(t.get("activities_summary", "Resumen de Actividades y Progreso"), expanded=True):
319
- total_entries = len(student_data['entries'])
320
- st.write(f"{t.get('total_analyses', 'Total de análisis realizados')}: {total_entries}")
321
-
322
- # Gráfico de tipos de análisis
323
- analysis_types = [entry['analysis_type'] for entry in student_data['entries']]
324
- analysis_counts = pd.Series(analysis_types).value_counts()
325
-
326
- fig, ax = plt.subplots(figsize=(8, 4))
327
- analysis_counts.plot(kind='bar', ax=ax)
328
- ax.set_title(t.get("analysis_types_chart", "Tipos de análisis realizados"))
329
- ax.set_xlabel(t.get("analysis_type", "Tipo de análisis"))
330
- ax.set_ylabel(t.get("count", "Cantidad"))
331
- st.pyplot(fig)
332
-
333
- # Histórico de Análisis Morfosintácticos
334
- with st.expander(t.get("morphosyntax_history", "Histórico de Análisis Morfosintácticos")):
335
- morphosyntax_entries = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'morphosyntax']
336
- if not morphosyntax_entries:
337
- st.warning("No se encontraron análisis morfosintácticos.")
338
- for entry in morphosyntax_entries:
339
- st.subheader(f"{t.get('analysis_of', 'Análisis del')} {entry['timestamp']}")
340
- if 'arc_diagrams' in entry and entry['arc_diagrams']:
341
- try:
342
- st.write(entry['arc_diagrams'][0], unsafe_allow_html=True)
343
- except Exception as e:
344
- logger.error(f"Error al mostrar diagrama de arco: {str(e)}")
345
- st.error("Error al mostrar el diagrama de arco.")
346
- else:
347
- st.write(t.get("no_arc_diagram", "No se encontró diagrama de arco para este análisis."))
348
-
349
- # Histórico de Análisis Semánticos
350
- with st.expander(t.get("semantic_history", "Histórico de Análisis Semánticos")):
351
- semantic_entries = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'semantic']
352
- if not semantic_entries:
353
- st.warning("No se encontraron análisis semánticos.")
354
- for entry in semantic_entries:
355
- st.subheader(f"{t.get('analysis_of', 'Análisis del')} {entry['timestamp']}")
356
- if 'key_concepts' in entry:
357
- st.write(t.get("key_concepts", "Conceptos clave:"))
358
- concepts_str = " | ".join([f"{concept} ({frequency:.2f})" for concept, frequency in entry['key_concepts']])
359
- st.markdown(f"<div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px;'>{concepts_str}</div>", unsafe_allow_html=True)
360
- if 'graph' in entry:
361
- try:
362
- img_bytes = base64.b64decode(entry['graph'])
363
- st.image(img_bytes, caption=t.get("conceptual_relations_graph", "Gráfico de relaciones conceptuales"))
364
- except Exception as e:
365
- logger.error(f"Error al mostrar gráfico semántico: {str(e)}")
366
- st.error(t.get("graph_display_error", f"No se pudo mostrar el gráfico: {str(e)}"))
367
-
368
- # Histórico de Análisis Discursivos
369
- with st.expander(t.get("discourse_history", "Histórico de Análisis Discursivos")):
370
- discourse_entries = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'discourse']
371
- for entry in discourse_entries:
372
- st.subheader(f"{t.get('analysis_of', 'Análisis del')} {entry['timestamp']}")
373
- for i in [1, 2]:
374
- if f'key_concepts{i}' in entry:
375
- st.write(f"{t.get('key_concepts', 'Conceptos clave')} {t.get('document', 'documento')} {i}:")
376
- concepts_str = " | ".join([f"{concept} ({frequency:.2f})" for concept, frequency in entry[f'key_concepts{i}']])
377
- st.markdown(f"<div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px;'>{concepts_str}</div>", unsafe_allow_html=True)
378
- try:
379
- if 'combined_graph' in entry and entry['combined_graph']:
380
- img_bytes = base64.b64decode(entry['combined_graph'])
381
- st.image(img_bytes, caption=t.get("combined_graph", "Gráfico combinado"))
382
- elif 'graph1' in entry and 'graph2' in entry:
383
- col1, col2 = st.columns(2)
384
- with col1:
385
- if entry['graph1']:
386
- img_bytes1 = base64.b64decode(entry['graph1'])
387
- st.image(img_bytes1, caption=t.get("graph_doc1", "Gráfico documento 1"))
388
- with col2:
389
- if entry['graph2']:
390
- img_bytes2 = base64.b64decode(entry['graph2'])
391
- st.image(img_bytes2, caption=t.get("graph_doc2", "Gráfico documento 2"))
392
- except Exception as e:
393
- st.error(t.get("graph_display_error", f"No se pudieron mostrar los gráficos: {str(e)}"))
394
-
395
- # Histórico de Conversaciones con el ChatBot
396
- with st.expander(t.get("chatbot_history", "Histórico de Conversaciones con el ChatBot")):
397
- if 'chat_history' in student_data and student_data['chat_history']:
398
- for i, chat in enumerate(student_data['chat_history']):
399
- st.subheader(f"{t.get('conversation', 'Conversación')} {i+1} - {chat['timestamp']}")
400
- for message in chat['messages']:
401
- if message['role'] == 'user':
402
- st.write(f"{t.get('user', 'Usuario')}: {message['content']}")
403
- else:
404
- st.write(f"{t.get('assistant', 'Asistente')}: {message['content']}")
405
- st.write("---")
406
- else:
407
- st.write(t.get("no_chat_history", "No se encontraron conversaciones con el ChatBot."))
408
-
409
- # Añadir logs para depuración
410
- if st.checkbox(t.get("show_debug_data", "Mostrar datos de depuración")):
411
- st.write(t.get("student_debug_data", "Datos del estudiante (para depuración):"))
412
- st.json(student_data)
413
-
414
- # Mostrar conteo de tipos de análisis
415
- analysis_types = [entry['analysis_type'] for entry in student_data['entries']]
416
- type_counts = {t: analysis_types.count(t) for t in set(analysis_types)}
417
- st.write("Conteo de tipos de análisis:")
418
- st.write(type_counts)
419
-
420
-
421
- #############################--- Update 16:00 24-9 #########################################
422
- def display_student_progress(username, lang_code, t, student_data):
423
- try:
424
- st.subheader(t.get('student_activities', 'Student Activitie'))
425
-
426
- if not student_data or all(len(student_data.get(key, [])) == 0 for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']):
427
- st.warning(t.get('no_data_warning', 'No analysis data found for this student.'))
428
- return
429
-
430
- # Resumen de actividades
431
- total_analyses = sum(len(student_data.get(key, [])) for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses'])
432
- st.write(f"{t.get('total_analyses', 'Total analyses performed')}: {total_analyses}")
433
-
434
- # Gráfico de tipos de análisis
435
- analysis_counts = {
436
- t.get('morpho_analyses', 'Morphosyntactic Analyses'): len(student_data.get('morphosyntax_analyses', [])),
437
- t.get('semantic_analyses', 'Semantic Analyses'): len(student_data.get('semantic_analyses', [])),
438
- t.get('discourse_analyses', 'Discourse Analyses'): len(student_data.get('discourse_analyses', []))
439
- }
440
- # Configurar el estilo de seaborn para un aspecto más atractivo
441
- sns.set_style("whitegrid")
442
-
443
- # Crear una figura más pequeña
444
- fig, ax = plt.subplots(figsize=(6, 4))
445
-
446
- # Usar colores más atractivos
447
- colors = ['#ff9999', '#66b3ff', '#99ff99']
448
-
449
- # Crear el gráfico de barras
450
- bars = ax.bar(analysis_counts.keys(), analysis_counts.values(), color=colors)
451
-
452
- # Añadir etiquetas de valor encima de cada barra
453
- for bar in bars:
454
- height = bar.get_height()
455
- ax.text(bar.get_x() + bar.get_width()/2., height,
456
- f'{height}',
457
- ha='center', va='bottom')
458
-
459
- # Configurar el título y las etiquetas
460
- ax.set_title(t.get('analysis_types_chart', 'Types of analyses performed'), fontsize=12)
461
- ax.set_ylabel(t.get('count', 'Count'), fontsize=10)
462
-
463
- # Rotar las etiquetas del eje x para mejor legibilidad
464
- plt.xticks(rotation=45, ha='right')
465
-
466
- # Ajustar el diseño para que todo quepa
467
- plt.tight_layout()
468
-
469
- # Mostrar el gráfico en Streamlit
470
- st.pyplot(fig)
471
-
472
- # Mostrar los últimos análisis
473
- for analysis_type in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']:
474
- with st.expander(t.get(f'{analysis_type}_expander', f'{analysis_type.capitalize()} History')):
475
- for analysis in student_data.get(analysis_type, [])[:5]: # Mostrar los últimos 5
476
- st.subheader(f"{t.get('analysis_from', 'Analysis from')} {analysis.get('timestamp', 'N/A')}")
477
- if analysis_type == 'morphosyntax_analyses':
478
- if 'arc_diagrams' in analysis:
479
- st.write(analysis['arc_diagrams'][0], unsafe_allow_html=True)
480
- elif analysis_type == 'semantic_analyses':
481
- if 'key_concepts' in analysis:
482
- st.write(t.get('key_concepts', 'Key concepts'))
483
- st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis['key_concepts']]))
484
- if 'graph' in analysis:
485
- st.image(base64.b64decode(analysis['graph']))
486
- elif analysis_type == 'discourse_analyses':
487
- for i in [1, 2]:
488
- if f'key_concepts{i}' in analysis:
489
- st.write(f"{t.get('key_concepts', 'Key concepts')} {t.get('document', 'Document')} {i}")
490
- st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis[f'key_concepts{i}']]))
491
- if 'combined_graph' in analysis:
492
- st.image(base64.b64decode(analysis['combined_graph']))
493
-
494
- # Mostrar el historial de chat
495
- with st.expander(t.get('chat_history_expander', 'Chat History')):
496
- for chat in student_data.get('chat_history', [])[:5]: # Mostrar las últimas 5 conversaciones
497
- st.subheader(f"{t.get('chat_from', 'Chat from')} {chat.get('timestamp', 'N/A')}")
498
- for message in chat.get('messages', []):
499
- st.write(f"{message.get('role', 'Unknown').capitalize()}: {message.get('content', 'No content')}")
500
- st.write("---")
501
-
502
- except Exception as e:
503
- logger.error(f"Error in display_student_progress: {str(e)}", exc_info=True)
504
- st.error(t.get('error_loading_progress', 'Error loading student progress. Please try again later.'))
505
-
506
-
507
-
508
-
509
-
510
-
511
-
512
-
513
-
514
-
515
-
516
-
517
-
518
-
519
-
520
-
521
-
522
-
523
-
524
-
525
-
526
-
527
-
528
-
529
-
530
-
531
-
532
- #####################################################################
533
- def display_student_progress(username, lang_code, t, student_data):
534
- st.subheader(t['student_progress'])
535
-
536
- if not student_data or all(len(student_data[key]) == 0 for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']):
537
- st.warning(t['no_data_warning'])
538
- return
539
-
540
- # Resumen de actividades
541
- total_analyses = sum(len(student_data[key]) for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses'])
542
- st.write(f"{t['total_analyses']}: {total_analyses}")
543
-
544
- # Gráfico de tipos de análisis
545
- analysis_counts = {
546
- t['morpho_analyses']: len(student_data['morphosyntax_analyses']),
547
- t['semantic_analyses']: len(student_data['semantic_analyses']),
548
- t['discourse_analyses']: len(student_data['discourse_analyses'])
549
- }
550
- fig, ax = plt.subplots()
551
- ax.bar(analysis_counts.keys(), analysis_counts.values())
552
- ax.set_title(t['analysis_types_chart'])
553
- st.pyplot(fig)
554
-
555
- # Mostrar los últimos análisis
556
- for analysis_type in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']:
557
- with st.expander(t[f'{analysis_type}_expander']):
558
- for analysis in student_data[analysis_type][:5]: # Mostrar los últimos 5
559
- st.subheader(f"{t['analysis_from']} {analysis['timestamp']}")
560
- if analysis_type == 'morphosyntax_analyses':
561
- if 'arc_diagrams' in analysis:
562
- st.write(analysis['arc_diagrams'][0], unsafe_allow_html=True)
563
- elif analysis_type == 'semantic_analyses':
564
- if 'key_concepts' in analysis:
565
- st.write(t['key_concepts'])
566
- st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis['key_concepts']]))
567
- if 'graph' in analysis:
568
- st.image(base64.b64decode(analysis['graph']))
569
- elif analysis_type == 'discourse_analyses':
570
- for i in [1, 2]:
571
- if f'key_concepts{i}' in analysis:
572
- st.write(f"{t['key_concepts']} {t['document']} {i}")
573
- st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis[f'key_concepts{i}']]))
574
- if 'combined_graph' in analysis:
575
- st.image(base64.b64decode(analysis['combined_graph']))
576
-
577
- # Mostrar el historial de chat
578
- with st.expander(t['chat_history_expander']):
579
- for chat in student_data['chat_history'][:5]: # Mostrar las últimas 5 conversaciones
580
- st.subheader(f"{t['chat_from']} {chat['timestamp']}")
581
- for message in chat['messages']:
582
- st.write(f"{message['role'].capitalize()}: {message['content']}")
583
- st.write("---")
584
-
585
-
586
-
587
- def display_student_progress(username, lang_code, t, student_data):
588
- st.subheader(t['student_activities'])
589
-
590
- if not student_data or all(len(student_data[key]) == 0 for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']):
591
- st.warning(t['no_data_warning'])
592
- return
593
-
594
- # Resumen de actividades
595
- total_analyses = sum(len(student_data[key]) for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses'])
596
- st.write(f"{t['total_analyses']}: {total_analyses}")
597
-
598
- # Gráfico de tipos de análisis
599
- analysis_counts = {
600
- t['morphological_analysis']: len(student_data['morphosyntax_analyses']),
601
- t['semantic_analyses']: len(student_data['semantic_analyses']),
602
- t['discourse_analyses']: len(student_data['discourse_analyses'])
603
- }
604
- fig, ax = plt.subplots()
605
- ax.bar(analysis_counts.keys(), analysis_counts.values())
606
- ax.set_title(t['analysis_types_chart'])
607
- st.pyplot(fig)
608
-
609
- # Mostrar los últimos análisis
610
- for analysis_type in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']:
611
- with st.expander(t[f'{analysis_type}_expander']):
612
- for analysis in student_data[analysis_type][:5]: # Mostrar los últimos 5
613
- st.subheader(f"{t['analysis_from']} {analysis['timestamp']}")
614
- if analysis_type == 'morphosyntax_analyses':
615
- if 'arc_diagrams' in analysis:
616
- st.write(analysis['arc_diagrams'][0], unsafe_allow_html=True)
617
- elif analysis_type == 'semantic_analyses':
618
- if 'key_concepts' in analysis:
619
- st.write(t['key_concepts'])
620
- st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis['key_concepts']]))
621
- if 'graph' in analysis:
622
- st.image(base64.b64decode(analysis['graph']))
623
- elif analysis_type == 'discourse_analyses':
624
- for i in [1, 2]:
625
- if f'key_concepts{i}' in analysis:
626
- st.write(f"{t['key_concepts']} {t['document']} {i}")
627
- st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis[f'key_concepts{i}']]))
628
- if 'combined_graph' in analysis:
629
- st.image(base64.b64decode(analysis['combined_graph']))
630
-
631
- # Mostrar el historial de chat
632
- with st.expander(t['chat_history_expander']):
633
- for chat in student_data['chat_history'][:5]: # Mostrar las últimas 5 conversaciones
634
- st.subheader(f"{t['chat_from']} {chat['timestamp']}")
635
- for message in chat['messages']:
636
- st.write(f"{message['role'].capitalize()}: {message['content']}")
637
- st.write("---")
638
-
639
-
640
-
641
-
642
- def display_student_progress(username, lang_code, t, student_data):
643
- st.subheader(t['student_activities'])
644
-
645
- if not student_data or all(len(student_data[key]) == 0 for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']):
646
- st.warning(t['no_data_warning'])
647
- return
648
-
649
- # Resumen de actividades
650
- total_analyses = sum(len(student_data[key]) for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses'])
651
- st.write(f"{t['total_analyses']}: {total_analyses}")
652
-
653
- # Gráfico de tipos de análisis
654
- analysis_counts = {
655
- t['morphological_analysis']: len(student_data['morphosyntax_analyses']),
656
- t['semantic_analyses']: len(student_data['semantic_analyses']),
657
- t['discourse_analyses']: len(student_data['discourse_analyses'])
658
- }
659
- fig, ax = plt.subplots()
660
- ax.bar(analysis_counts.keys(), analysis_counts.values())
661
- ax.set_title(t['analysis_types_chart'])
662
- st.pyplot(fig)
663
-
664
- # Mostrar los últimos análisis
665
- for analysis_type in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']:
666
- with st.expander(t[f'{analysis_type}_expander']):
667
- for analysis in student_data[analysis_type][:5]: # Mostrar los últimos 5
668
- st.subheader(f"{t['analysis_from']} {analysis['timestamp']}")
669
- if analysis_type == 'morphosyntax_analyses':
670
- if 'arc_diagrams' in analysis:
671
- st.write(analysis['arc_diagrams'][0], unsafe_allow_html=True)
672
- elif analysis_type == 'semantic_analyses':
673
- if 'key_concepts' in analysis:
674
- st.write(t['key_concepts'])
675
- st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis['key_concepts']]))
676
- if 'graph' in analysis:
677
- st.image(base64.b64decode(analysis['graph']))
678
- elif analysis_type == 'discourse_analyses':
679
- for i in [1, 2]:
680
- if f'key_concepts{i}' in analysis:
681
- st.write(f"{t['key_concepts']} {t['document']} {i}")
682
- st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis[f'key_concepts{i}']]))
683
- if 'combined_graph' in analysis:
684
- st.image(base64.b64decode(analysis['combined_graph']))
685
-
686
- # Mostrar el historial de chat
687
- with st.expander(t['chat_history_expander']):
688
- for chat in student_data['chat_history'][:5]: # Mostrar las últimas 5 conversaciones
689
- st.subheader(f"{t['chat_from']} {chat['timestamp']}")
690
- for message in chat['messages']:
691
- st.write(f"{message['role'].capitalize()}: {message['content']}")
692
- st.write("---")
693
-
694
-
695
-
696
-
697
- def display_student_progress(username, lang_code, t):
698
- st.subheader(t['student_activities'])
699
- st.write(f"{t['activities_message']} {username}")
700
-
701
- # Aquí puedes agregar más contenido estático o placeholder
702
- st.info(t['activities_placeholder'])
703
-
704
- # Si necesitas mostrar algún dato, puedes usar datos de ejemplo o placeholders
705
- col1, col2, col3 = st.columns(3)
706
- col1.metric(t['morpho_analyses'], "5") # Ejemplo de dato
707
- col2.metric(t['semantic_analyses'], "3") # Ejemplo de dato
708
- col3.metric(t['discourse_analyses'], "2") # Ejemplo de dato
709
-
710
-
711
-
712
- def display_student_progress(username, lang_code, t):
713
- st.title(f"Actividades de {username}")
714
-
715
- # Obtener todos los datos del estudiante
716
- student_data = get_student_data(username)
717
-
718
- if not student_data or len(student_data.get('entries', [])) == 0:
719
- st.warning("No se encontraron datos de análisis para este estudiante.")
720
- st.info("Intenta realizar algunos análisis de texto primero.")
721
- return
722
-
723
- # Resumen de actividades
724
- with st.expander("Resumen de Actividades", expanded=True):
725
- total_entries = len(student_data['entries'])
726
- st.write(f"Total de análisis realizados: {total_entries}")
727
-
728
- # Gráfico de tipos de análisis
729
- analysis_types = [entry['analysis_type'] for entry in student_data['entries']]
730
- analysis_counts = pd.Series(analysis_types).value_counts()
731
- fig, ax = plt.subplots()
732
- analysis_counts.plot(kind='bar', ax=ax)
733
- ax.set_title("Tipos de análisis realizados")
734
- ax.set_xlabel("Tipo de análisis")
735
- ax.set_ylabel("Cantidad")
736
- st.pyplot(fig)
737
-
738
- # Histórico de Análisis Morfosintácticos
739
- with st.expander("Histórico de Análisis Morfosintácticos"):
740
- morpho_analyses = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'morphosyntax']
741
- for analysis in morpho_analyses[:5]: # Mostrar los últimos 5
742
- st.subheader(f"Análisis del {analysis['timestamp']}")
743
- if 'arc_diagrams' in analysis:
744
- st.write(analysis['arc_diagrams'][0], unsafe_allow_html=True)
745
-
746
- # Histórico de Análisis Semánticos
747
- with st.expander("Histórico de Análisis Semánticos"):
748
- semantic_analyses = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'semantic']
749
- for analysis in semantic_analyses[:5]: # Mostrar los últimos 5
750
- st.subheader(f"Análisis del {analysis['timestamp']}")
751
- if 'key_concepts' in analysis:
752
- concepts_str = " | ".join([f"{concept} ({frequency:.2f})" for concept, frequency in analysis['key_concepts']])
753
- st.markdown(f"<div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px;'>{concepts_str}</div>", unsafe_allow_html=True)
754
- if 'graph' in analysis:
755
- try:
756
- img_bytes = base64.b64decode(analysis['graph'])
757
- st.image(img_bytes, caption="Gráfico de relaciones conceptuales")
758
- except Exception as e:
759
- st.error(f"No se pudo mostrar el gráfico: {str(e)}")
760
-
761
- # Histórico de Análisis Discursivos
762
- with st.expander("Histórico de Análisis Discursivos"):
763
- discourse_analyses = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'discourse']
764
- for analysis in discourse_analyses[:5]: # Mostrar los últimos 5
765
- st.subheader(f"Análisis del {analysis['timestamp']}")
766
- for i in [1, 2]:
767
- if f'key_concepts{i}' in analysis:
768
- concepts_str = " | ".join([f"{concept} ({frequency:.2f})" for concept, frequency in analysis[f'key_concepts{i}']])
769
- st.write(f"Conceptos clave del documento {i}:")
770
- st.markdown(f"<div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px;'>{concepts_str}</div>", unsafe_allow_html=True)
771
- if 'combined_graph' in analysis:
772
- try:
773
- img_bytes = base64.b64decode(analysis['combined_graph'])
774
- st.image(img_bytes)
775
- except Exception as e:
776
- st.error(f"No se pudo mostrar el gráfico combinado: {str(e)}")
777
-
778
- # Histórico de Conversaciones con el ChatBot
779
- with st.expander("Histórico de Conversaciones con el ChatBot"):
780
- if 'chat_history' in student_data:
781
- for i, chat in enumerate(student_data['chat_history'][:5]): # Mostrar las últimas 5 conversaciones
782
- st.subheader(f"Conversación {i+1} - {chat['timestamp']}")
783
- for message in chat['messages']:
784
- st.write(f"{message['role'].capitalize()}: {message['content']}")
785
- st.write("---")
786
- else:
787
- st.write("No se encontraron conversaciones con el ChatBot.")
788
-
789
- # Opción para mostrar datos de depuración
790
- if st.checkbox("Mostrar datos de depuración"):
791
- st.write("Datos del estudiante (para depuración):")
792
- st.json(student_data)
793
-
794
  '''
 
1
+ ##############
2
+ ###modules/studentact/student_activities_v2.py
3
+
4
+ import streamlit as st
5
+ import re
6
+ import io
7
+ from io import BytesIO
8
+ import pandas as pd
9
+ import numpy as np
10
+ import time
11
+ import matplotlib.pyplot as plt
12
+ from datetime import datetime
13
+ from spacy import displacy
14
+ import random
15
+ import base64
16
+ import seaborn as sns
17
+ import logging
18
+
19
+ # Importaciones de la base de datos
20
+ from ..database.morphosintax_mongo_db import get_student_morphosyntax_analysis
21
+ from ..database.semantic_mongo_db import get_student_semantic_analysis
22
+ from ..database.discourse_mongo_db import get_student_discourse_analysis
23
+ from ..database.chat_mongo_db import get_chat_history
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ ###################################################################################
28
+
29
+ def display_student_activities(username: str, lang_code: str, t: dict):
30
+ """
31
+ Muestra todas las actividades del estudiante
32
+ Args:
33
+ username: Nombre del estudiante
34
+ lang_code: Código del idioma
35
+ t: Diccionario de traducciones
36
+ """
37
+ try:
38
+ st.header(t.get('activities_title', 'Mis Actividades'))
39
+
40
+ # Tabs para diferentes tipos de análisis
41
+ tabs = st.tabs([
42
+ t.get('morpho_activities', 'Análisis Morfosintáctico'),
43
+ t.get('semantic_activities', 'Análisis Semántico'),
44
+ t.get('discourse_activities', 'Análisis del Discurso'),
45
+ t.get('chat_activities', 'Conversaciones con el Asistente')
46
+ ])
47
+
48
+ # Tab de Análisis Morfosintáctico
49
+ with tabs[0]:
50
+ display_morphosyntax_activities(username, t)
51
+
52
+ # Tab de Análisis Semántico
53
+ with tabs[1]:
54
+ display_semantic_activities(username, t)
55
+
56
+ # Tab de Análisis del Discurso
57
+ with tabs[2]:
58
+ display_discourse_activities(username, t)
59
+
60
+ # Tab de Conversaciones del Chat
61
+ with tabs[3]:
62
+ display_chat_activities(username, t)
63
+
64
+ except Exception as e:
65
+ logger.error(f"Error mostrando actividades: {str(e)}")
66
+ st.error(t.get('error_loading_activities', 'Error al cargar las actividades'))
67
+
68
+
69
+ ###############################################################################################
70
+ def display_morphosyntax_activities(username: str, t: dict):
71
+ """Muestra actividades de análisis morfosintáctico"""
72
+ try:
73
+ analyses = get_student_morphosyntax_analysis(username)
74
+ if not analyses:
75
+ st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
76
+ return
77
+
78
+ for analysis in analyses:
79
+ with st.expander(
80
+ f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
81
+ expanded=False
82
+ ):
83
+ st.text(f"{t.get('analyzed_text', 'Texto analizado')}:")
84
+ st.write(analysis['text'])
85
+
86
+ if 'arc_diagrams' in analysis:
87
+ st.subheader(t.get('syntactic_diagrams', 'Diagramas sintácticos'))
88
+ for diagram in analysis['arc_diagrams']:
89
+ st.write(diagram, unsafe_allow_html=True)
90
+
91
+ except Exception as e:
92
+ logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
93
+ st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
94
+
95
+
96
+ ###############################################################################################
97
+ def display_semantic_activities(username: str, t: dict):
98
+ """Muestra actividades de análisis semántico"""
99
+ try:
100
+ logger.info(f"Recuperando análisis semántico para {username}")
101
+ analyses = get_student_semantic_analysis(username)
102
+
103
+ if not analyses:
104
+ logger.info("No se encontraron análisis semánticos")
105
+ st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
106
+ return
107
+
108
+ logger.info(f"Procesando {len(analyses)} análisis semánticos")
109
+ for analysis in analyses:
110
+ try:
111
+ # Verificar campos mínimos necesarios
112
+ if not all(key in analysis for key in ['timestamp', 'concept_graph']):
113
+ logger.warning(f"Análisis incompleto: {analysis.keys()}")
114
+ continue
115
+
116
+ # Formatear fecha
117
+ timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
118
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
119
+
120
+ with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
121
+ if analysis['concept_graph']:
122
+ logger.debug("Decodificando gráfico de conceptos")
123
+ try:
124
+ image_bytes = base64.b64decode(analysis['concept_graph'])
125
+ st.image(image_bytes, use_column_width=True)
126
+ logger.debug("Gráfico mostrado exitosamente")
127
+ except Exception as img_error:
128
+ logger.error(f"Error decodificando imagen: {str(img_error)}")
129
+ st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
130
+ else:
131
+ st.info(t.get('no_graph', 'No hay visualización disponible'))
132
+
133
+ except Exception as e:
134
+ logger.error(f"Error procesando análisis individual: {str(e)}")
135
+ continue
136
+
137
+ except Exception as e:
138
+ logger.error(f"Error mostrando análisis semántico: {str(e)}")
139
+ st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
140
+
141
+
142
+ ###################################################################################################
143
+ def display_discourse_activities(username: str, t: dict):
144
+ """Muestra actividades de análisis del discurso"""
145
+ try:
146
+ logger.info(f"Recuperando análisis del discurso para {username}")
147
+ analyses = get_student_discourse_analysis(username)
148
+
149
+ if not analyses:
150
+ logger.info("No se encontraron análisis del discurso")
151
+ st.info(t.get('no_discourse_analyses', 'No hay análisis del discurso registrados'))
152
+ return
153
+
154
+ logger.info(f"Procesando {len(analyses)} análisis del discurso")
155
+ for analysis in analyses:
156
+ try:
157
+ # Verificar campos mínimos necesarios
158
+ if not all(key in analysis for key in ['timestamp', 'combined_graph']):
159
+ logger.warning(f"Análisis incompleto: {analysis.keys()}")
160
+ continue
161
+
162
+ # Formatear fecha
163
+ timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
164
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
165
+
166
+ with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
167
+ if analysis['combined_graph']:
168
+ logger.debug("Decodificando gráfico combinado")
169
+ try:
170
+ image_bytes = base64.b64decode(analysis['combined_graph'])
171
+ st.image(image_bytes, use_column_width=True)
172
+ logger.debug("Gráfico mostrado exitosamente")
173
+ except Exception as img_error:
174
+ logger.error(f"Error decodificando imagen: {str(img_error)}")
175
+ st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
176
+ else:
177
+ st.info(t.get('no_visualization', 'No hay visualización comparativa disponible'))
178
+
179
+ except Exception as e:
180
+ logger.error(f"Error procesando análisis individual: {str(e)}")
181
+ continue
182
+
183
+ except Exception as e:
184
+ logger.error(f"Error mostrando análisis del discurso: {str(e)}")
185
+ st.error(t.get('error_discourse', 'Error al mostrar análisis del discurso'))
186
+
187
+ #################################################################################
188
+ def display_discourse_comparison(analysis: dict, t: dict):
189
+ """Muestra la comparación de análisis del discurso"""
190
+ st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
191
+
192
+ col1, col2 = st.columns(2)
193
+ with col1:
194
+ st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
195
+ df1 = pd.DataFrame(analysis['key_concepts1'])
196
+ st.dataframe(df1)
197
+
198
+ with col2:
199
+ st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
200
+ df2 = pd.DataFrame(analysis['key_concepts2'])
201
+ st.dataframe(df2)
202
+
203
+ #################################################################################
204
+ def display_chat_activities(username: str, t: dict):
205
+ """
206
+ Muestra historial de conversaciones del chat
207
+ """
208
+ try:
209
+ # Obtener historial del chat
210
+ chat_history = get_chat_history(
211
+ username=username,
212
+ analysis_type='sidebar',
213
+ limit=50
214
+ )
215
+
216
+ if not chat_history:
217
+ st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
218
+ return
219
+
220
+ for chat in reversed(chat_history): # Mostrar las más recientes primero
221
+ try:
222
+ # Convertir timestamp a datetime para formato
223
+ timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
224
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
225
+
226
+ with st.expander(
227
+ f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
228
+ expanded=False
229
+ ):
230
+ if 'messages' in chat and chat['messages']:
231
+ # Mostrar cada mensaje en la conversación
232
+ for message in chat['messages']:
233
+ role = message.get('role', 'unknown')
234
+ content = message.get('content', '')
235
+
236
+ # Usar el componente de chat de Streamlit
237
+ with st.chat_message(role):
238
+ st.markdown(content)
239
+
240
+ # Agregar separador entre mensajes
241
+ st.divider()
242
+ else:
243
+ st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
244
+
245
+ except Exception as e:
246
+ logger.error(f"Error mostrando conversación: {str(e)}")
247
+ continue
248
+
249
+ except Exception as e:
250
+ logger.error(f"Error mostrando historial del chat: {str(e)}")
251
+ st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
252
+
253
+
254
+
255
+
256
+
257
+
258
+
259
+
260
+
261
+ '''
262
+ ##########versión 25-9-2024---02:30 ################ OK (username)####################
263
+
264
+ def display_student_progress(username, lang_code, t, student_data):
265
+ st.title(f"{t.get('progress_of', 'Progreso de')} {username}")
266
+
267
+ if not student_data or len(student_data.get('entries', [])) == 0:
268
+ st.warning(t.get("no_data_warning", "No se encontraron datos para este estudiante."))
269
+ st.info(t.get("try_analysis", "Intenta realizar algunos análisis de texto primero."))
270
+ return
271
+
272
+ with st.expander(t.get("activities_summary", "Resumen de Actividades"), expanded=True):
273
+ total_entries = len(student_data['entries'])
274
+ st.write(f"{t.get('total_analyses', 'Total de análisis realizados')}: {total_entries}")
275
+
276
+ # Gráfico de tipos de análisis
277
+ analysis_types = [entry['analysis_type'] for entry in student_data['entries']]
278
+ analysis_counts = pd.Series(analysis_types).value_counts()
279
+ fig, ax = plt.subplots()
280
+ analysis_counts.plot(kind='bar', ax=ax)
281
+ ax.set_title(t.get("analysis_types_chart", "Tipos de análisis realizados"))
282
+ ax.set_xlabel(t.get("analysis_type", "Tipo de análisis"))
283
+ ax.set_ylabel(t.get("count", "Cantidad"))
284
+ st.pyplot(fig)
285
+
286
+ # Mostrar los últimos análisis morfosintácticos
287
+ with st.expander(t.get("morphosyntax_history", "Histórico de Análisis Morfosintácticos")):
288
+ morphosyntax_entries = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'morphosyntax']
289
+ for entry in morphosyntax_entries[:5]: # Mostrar los últimos 5
290
+ st.subheader(f"{t.get('analysis_of', 'Análisis del')} {entry['timestamp']}")
291
+ if 'arc_diagrams' in entry and entry['arc_diagrams']:
292
+ st.components.v1.html(entry['arc_diagrams'][0], height=300, scrolling=True)
293
+
294
+ # Añadir secciones similares para análisis semánticos y discursivos si es necesario
295
+
296
+ # Mostrar el historial de chat
297
+ with st.expander(t.get("chat_history", "Historial de Chat")):
298
+ if 'chat_history' in student_data:
299
+ for chat in student_data['chat_history'][:5]: # Mostrar las últimas 5 conversaciones
300
+ st.subheader(f"{t.get('chat_from', 'Chat del')} {chat['timestamp']}")
301
+ for message in chat['messages']:
302
+ st.write(f"{message['role'].capitalize()}: {message['content']}")
303
+ st.write("---")
304
+ else:
305
+ st.write(t.get("no_chat_history", "No hay historial de chat disponible."))
306
+
307
+
308
+ ##########versión 24-9-2024---17:30 ################ OK FROM--V2 de def get_student_data(username)####################
309
+
310
+ def display_student_progress(username, lang_code, t, student_data):
311
+ if not student_data or len(student_data['entries']) == 0:
312
+ st.warning(t.get("no_data_warning", "No se encontraron datos para este estudiante."))
313
+ st.info(t.get("try_analysis", "Intenta realizar algunos análisis de texto primero."))
314
+ return
315
+
316
+ st.title(f"{t.get('progress_of', 'Progreso de')} {username}")
317
+
318
+ with st.expander(t.get("activities_summary", "Resumen de Actividades y Progreso"), expanded=True):
319
+ total_entries = len(student_data['entries'])
320
+ st.write(f"{t.get('total_analyses', 'Total de análisis realizados')}: {total_entries}")
321
+
322
+ # Gráfico de tipos de análisis
323
+ analysis_types = [entry['analysis_type'] for entry in student_data['entries']]
324
+ analysis_counts = pd.Series(analysis_types).value_counts()
325
+
326
+ fig, ax = plt.subplots(figsize=(8, 4))
327
+ analysis_counts.plot(kind='bar', ax=ax)
328
+ ax.set_title(t.get("analysis_types_chart", "Tipos de análisis realizados"))
329
+ ax.set_xlabel(t.get("analysis_type", "Tipo de análisis"))
330
+ ax.set_ylabel(t.get("count", "Cantidad"))
331
+ st.pyplot(fig)
332
+
333
+ # Histórico de Análisis Morfosintácticos
334
+ with st.expander(t.get("morphosyntax_history", "Histórico de Análisis Morfosintácticos")):
335
+ morphosyntax_entries = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'morphosyntax']
336
+ if not morphosyntax_entries:
337
+ st.warning("No se encontraron análisis morfosintácticos.")
338
+ for entry in morphosyntax_entries:
339
+ st.subheader(f"{t.get('analysis_of', 'Análisis del')} {entry['timestamp']}")
340
+ if 'arc_diagrams' in entry and entry['arc_diagrams']:
341
+ try:
342
+ st.write(entry['arc_diagrams'][0], unsafe_allow_html=True)
343
+ except Exception as e:
344
+ logger.error(f"Error al mostrar diagrama de arco: {str(e)}")
345
+ st.error("Error al mostrar el diagrama de arco.")
346
+ else:
347
+ st.write(t.get("no_arc_diagram", "No se encontró diagrama de arco para este análisis."))
348
+
349
+ # Histórico de Análisis Semánticos
350
+ with st.expander(t.get("semantic_history", "Histórico de Análisis Semánticos")):
351
+ semantic_entries = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'semantic']
352
+ if not semantic_entries:
353
+ st.warning("No se encontraron análisis semánticos.")
354
+ for entry in semantic_entries:
355
+ st.subheader(f"{t.get('analysis_of', 'Análisis del')} {entry['timestamp']}")
356
+ if 'key_concepts' in entry:
357
+ st.write(t.get("key_concepts", "Conceptos clave:"))
358
+ concepts_str = " | ".join([f"{concept} ({frequency:.2f})" for concept, frequency in entry['key_concepts']])
359
+ st.markdown(f"<div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px;'>{concepts_str}</div>", unsafe_allow_html=True)
360
+ if 'graph' in entry:
361
+ try:
362
+ img_bytes = base64.b64decode(entry['graph'])
363
+ st.image(img_bytes, caption=t.get("conceptual_relations_graph", "Gráfico de relaciones conceptuales"))
364
+ except Exception as e:
365
+ logger.error(f"Error al mostrar gráfico semántico: {str(e)}")
366
+ st.error(t.get("graph_display_error", f"No se pudo mostrar el gráfico: {str(e)}"))
367
+
368
+ # Histórico de Análisis Discursivos
369
+ with st.expander(t.get("discourse_history", "Histórico de Análisis Discursivos")):
370
+ discourse_entries = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'discourse']
371
+ for entry in discourse_entries:
372
+ st.subheader(f"{t.get('analysis_of', 'Análisis del')} {entry['timestamp']}")
373
+ for i in [1, 2]:
374
+ if f'key_concepts{i}' in entry:
375
+ st.write(f"{t.get('key_concepts', 'Conceptos clave')} {t.get('document', 'documento')} {i}:")
376
+ concepts_str = " | ".join([f"{concept} ({frequency:.2f})" for concept, frequency in entry[f'key_concepts{i}']])
377
+ st.markdown(f"<div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px;'>{concepts_str}</div>", unsafe_allow_html=True)
378
+ try:
379
+ if 'combined_graph' in entry and entry['combined_graph']:
380
+ img_bytes = base64.b64decode(entry['combined_graph'])
381
+ st.image(img_bytes, caption=t.get("combined_graph", "Gráfico combinado"))
382
+ elif 'graph1' in entry and 'graph2' in entry:
383
+ col1, col2 = st.columns(2)
384
+ with col1:
385
+ if entry['graph1']:
386
+ img_bytes1 = base64.b64decode(entry['graph1'])
387
+ st.image(img_bytes1, caption=t.get("graph_doc1", "Gráfico documento 1"))
388
+ with col2:
389
+ if entry['graph2']:
390
+ img_bytes2 = base64.b64decode(entry['graph2'])
391
+ st.image(img_bytes2, caption=t.get("graph_doc2", "Gráfico documento 2"))
392
+ except Exception as e:
393
+ st.error(t.get("graph_display_error", f"No se pudieron mostrar los gráficos: {str(e)}"))
394
+
395
+ # Histórico de Conversaciones con el ChatBot
396
+ with st.expander(t.get("chatbot_history", "Histórico de Conversaciones con el ChatBot")):
397
+ if 'chat_history' in student_data and student_data['chat_history']:
398
+ for i, chat in enumerate(student_data['chat_history']):
399
+ st.subheader(f"{t.get('conversation', 'Conversación')} {i+1} - {chat['timestamp']}")
400
+ for message in chat['messages']:
401
+ if message['role'] == 'user':
402
+ st.write(f"{t.get('user', 'Usuario')}: {message['content']}")
403
+ else:
404
+ st.write(f"{t.get('assistant', 'Asistente')}: {message['content']}")
405
+ st.write("---")
406
+ else:
407
+ st.write(t.get("no_chat_history", "No se encontraron conversaciones con el ChatBot."))
408
+
409
+ # Añadir logs para depuración
410
+ if st.checkbox(t.get("show_debug_data", "Mostrar datos de depuración")):
411
+ st.write(t.get("student_debug_data", "Datos del estudiante (para depuración):"))
412
+ st.json(student_data)
413
+
414
+ # Mostrar conteo de tipos de análisis
415
+ analysis_types = [entry['analysis_type'] for entry in student_data['entries']]
416
+ type_counts = {t: analysis_types.count(t) for t in set(analysis_types)}
417
+ st.write("Conteo de tipos de análisis:")
418
+ st.write(type_counts)
419
+
420
+
421
+ #############################--- Update 16:00 24-9 #########################################
422
+ def display_student_progress(username, lang_code, t, student_data):
423
+ try:
424
+ st.subheader(t.get('student_activities', 'Student Activitie'))
425
+
426
+ if not student_data or all(len(student_data.get(key, [])) == 0 for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']):
427
+ st.warning(t.get('no_data_warning', 'No analysis data found for this student.'))
428
+ return
429
+
430
+ # Resumen de actividades
431
+ total_analyses = sum(len(student_data.get(key, [])) for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses'])
432
+ st.write(f"{t.get('total_analyses', 'Total analyses performed')}: {total_analyses}")
433
+
434
+ # Gráfico de tipos de análisis
435
+ analysis_counts = {
436
+ t.get('morpho_analyses', 'Morphosyntactic Analyses'): len(student_data.get('morphosyntax_analyses', [])),
437
+ t.get('semantic_analyses', 'Semantic Analyses'): len(student_data.get('semantic_analyses', [])),
438
+ t.get('discourse_analyses', 'Discourse Analyses'): len(student_data.get('discourse_analyses', []))
439
+ }
440
+ # Configurar el estilo de seaborn para un aspecto más atractivo
441
+ sns.set_style("whitegrid")
442
+
443
+ # Crear una figura más pequeña
444
+ fig, ax = plt.subplots(figsize=(6, 4))
445
+
446
+ # Usar colores más atractivos
447
+ colors = ['#ff9999', '#66b3ff', '#99ff99']
448
+
449
+ # Crear el gráfico de barras
450
+ bars = ax.bar(analysis_counts.keys(), analysis_counts.values(), color=colors)
451
+
452
+ # Añadir etiquetas de valor encima de cada barra
453
+ for bar in bars:
454
+ height = bar.get_height()
455
+ ax.text(bar.get_x() + bar.get_width()/2., height,
456
+ f'{height}',
457
+ ha='center', va='bottom')
458
+
459
+ # Configurar el título y las etiquetas
460
+ ax.set_title(t.get('analysis_types_chart', 'Types of analyses performed'), fontsize=12)
461
+ ax.set_ylabel(t.get('count', 'Count'), fontsize=10)
462
+
463
+ # Rotar las etiquetas del eje x para mejor legibilidad
464
+ plt.xticks(rotation=45, ha='right')
465
+
466
+ # Ajustar el diseño para que todo quepa
467
+ plt.tight_layout()
468
+
469
+ # Mostrar el gráfico en Streamlit
470
+ st.pyplot(fig)
471
+
472
+ # Mostrar los últimos análisis
473
+ for analysis_type in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']:
474
+ with st.expander(t.get(f'{analysis_type}_expander', f'{analysis_type.capitalize()} History')):
475
+ for analysis in student_data.get(analysis_type, [])[:5]: # Mostrar los últimos 5
476
+ st.subheader(f"{t.get('analysis_from', 'Analysis from')} {analysis.get('timestamp', 'N/A')}")
477
+ if analysis_type == 'morphosyntax_analyses':
478
+ if 'arc_diagrams' in analysis:
479
+ st.write(analysis['arc_diagrams'][0], unsafe_allow_html=True)
480
+ elif analysis_type == 'semantic_analyses':
481
+ if 'key_concepts' in analysis:
482
+ st.write(t.get('key_concepts', 'Key concepts'))
483
+ st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis['key_concepts']]))
484
+ if 'graph' in analysis:
485
+ st.image(base64.b64decode(analysis['graph']))
486
+ elif analysis_type == 'discourse_analyses':
487
+ for i in [1, 2]:
488
+ if f'key_concepts{i}' in analysis:
489
+ st.write(f"{t.get('key_concepts', 'Key concepts')} {t.get('document', 'Document')} {i}")
490
+ st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis[f'key_concepts{i}']]))
491
+ if 'combined_graph' in analysis:
492
+ st.image(base64.b64decode(analysis['combined_graph']))
493
+
494
+ # Mostrar el historial de chat
495
+ with st.expander(t.get('chat_history_expander', 'Chat History')):
496
+ for chat in student_data.get('chat_history', [])[:5]: # Mostrar las últimas 5 conversaciones
497
+ st.subheader(f"{t.get('chat_from', 'Chat from')} {chat.get('timestamp', 'N/A')}")
498
+ for message in chat.get('messages', []):
499
+ st.write(f"{message.get('role', 'Unknown').capitalize()}: {message.get('content', 'No content')}")
500
+ st.write("---")
501
+
502
+ except Exception as e:
503
+ logger.error(f"Error in display_student_progress: {str(e)}", exc_info=True)
504
+ st.error(t.get('error_loading_progress', 'Error loading student progress. Please try again later.'))
505
+
506
+
507
+
508
+
509
+
510
+
511
+
512
+
513
+
514
+
515
+
516
+
517
+
518
+
519
+
520
+
521
+
522
+
523
+
524
+
525
+
526
+
527
+
528
+
529
+
530
+
531
+
532
+ #####################################################################
533
+ def display_student_progress(username, lang_code, t, student_data):
534
+ st.subheader(t['student_progress'])
535
+
536
+ if not student_data or all(len(student_data[key]) == 0 for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']):
537
+ st.warning(t['no_data_warning'])
538
+ return
539
+
540
+ # Resumen de actividades
541
+ total_analyses = sum(len(student_data[key]) for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses'])
542
+ st.write(f"{t['total_analyses']}: {total_analyses}")
543
+
544
+ # Gráfico de tipos de análisis
545
+ analysis_counts = {
546
+ t['morpho_analyses']: len(student_data['morphosyntax_analyses']),
547
+ t['semantic_analyses']: len(student_data['semantic_analyses']),
548
+ t['discourse_analyses']: len(student_data['discourse_analyses'])
549
+ }
550
+ fig, ax = plt.subplots()
551
+ ax.bar(analysis_counts.keys(), analysis_counts.values())
552
+ ax.set_title(t['analysis_types_chart'])
553
+ st.pyplot(fig)
554
+
555
+ # Mostrar los últimos análisis
556
+ for analysis_type in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']:
557
+ with st.expander(t[f'{analysis_type}_expander']):
558
+ for analysis in student_data[analysis_type][:5]: # Mostrar los últimos 5
559
+ st.subheader(f"{t['analysis_from']} {analysis['timestamp']}")
560
+ if analysis_type == 'morphosyntax_analyses':
561
+ if 'arc_diagrams' in analysis:
562
+ st.write(analysis['arc_diagrams'][0], unsafe_allow_html=True)
563
+ elif analysis_type == 'semantic_analyses':
564
+ if 'key_concepts' in analysis:
565
+ st.write(t['key_concepts'])
566
+ st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis['key_concepts']]))
567
+ if 'graph' in analysis:
568
+ st.image(base64.b64decode(analysis['graph']))
569
+ elif analysis_type == 'discourse_analyses':
570
+ for i in [1, 2]:
571
+ if f'key_concepts{i}' in analysis:
572
+ st.write(f"{t['key_concepts']} {t['document']} {i}")
573
+ st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis[f'key_concepts{i}']]))
574
+ if 'combined_graph' in analysis:
575
+ st.image(base64.b64decode(analysis['combined_graph']))
576
+
577
+ # Mostrar el historial de chat
578
+ with st.expander(t['chat_history_expander']):
579
+ for chat in student_data['chat_history'][:5]: # Mostrar las últimas 5 conversaciones
580
+ st.subheader(f"{t['chat_from']} {chat['timestamp']}")
581
+ for message in chat['messages']:
582
+ st.write(f"{message['role'].capitalize()}: {message['content']}")
583
+ st.write("---")
584
+
585
+
586
+
587
+ def display_student_progress(username, lang_code, t, student_data):
588
+ st.subheader(t['student_activities'])
589
+
590
+ if not student_data or all(len(student_data[key]) == 0 for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']):
591
+ st.warning(t['no_data_warning'])
592
+ return
593
+
594
+ # Resumen de actividades
595
+ total_analyses = sum(len(student_data[key]) for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses'])
596
+ st.write(f"{t['total_analyses']}: {total_analyses}")
597
+
598
+ # Gráfico de tipos de análisis
599
+ analysis_counts = {
600
+ t['morphological_analysis']: len(student_data['morphosyntax_analyses']),
601
+ t['semantic_analyses']: len(student_data['semantic_analyses']),
602
+ t['discourse_analyses']: len(student_data['discourse_analyses'])
603
+ }
604
+ fig, ax = plt.subplots()
605
+ ax.bar(analysis_counts.keys(), analysis_counts.values())
606
+ ax.set_title(t['analysis_types_chart'])
607
+ st.pyplot(fig)
608
+
609
+ # Mostrar los últimos análisis
610
+ for analysis_type in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']:
611
+ with st.expander(t[f'{analysis_type}_expander']):
612
+ for analysis in student_data[analysis_type][:5]: # Mostrar los últimos 5
613
+ st.subheader(f"{t['analysis_from']} {analysis['timestamp']}")
614
+ if analysis_type == 'morphosyntax_analyses':
615
+ if 'arc_diagrams' in analysis:
616
+ st.write(analysis['arc_diagrams'][0], unsafe_allow_html=True)
617
+ elif analysis_type == 'semantic_analyses':
618
+ if 'key_concepts' in analysis:
619
+ st.write(t['key_concepts'])
620
+ st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis['key_concepts']]))
621
+ if 'graph' in analysis:
622
+ st.image(base64.b64decode(analysis['graph']))
623
+ elif analysis_type == 'discourse_analyses':
624
+ for i in [1, 2]:
625
+ if f'key_concepts{i}' in analysis:
626
+ st.write(f"{t['key_concepts']} {t['document']} {i}")
627
+ st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis[f'key_concepts{i}']]))
628
+ if 'combined_graph' in analysis:
629
+ st.image(base64.b64decode(analysis['combined_graph']))
630
+
631
+ # Mostrar el historial de chat
632
+ with st.expander(t['chat_history_expander']):
633
+ for chat in student_data['chat_history'][:5]: # Mostrar las últimas 5 conversaciones
634
+ st.subheader(f"{t['chat_from']} {chat['timestamp']}")
635
+ for message in chat['messages']:
636
+ st.write(f"{message['role'].capitalize()}: {message['content']}")
637
+ st.write("---")
638
+
639
+
640
+
641
+
642
+ def display_student_progress(username, lang_code, t, student_data):
643
+ st.subheader(t['student_activities'])
644
+
645
+ if not student_data or all(len(student_data[key]) == 0 for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']):
646
+ st.warning(t['no_data_warning'])
647
+ return
648
+
649
+ # Resumen de actividades
650
+ total_analyses = sum(len(student_data[key]) for key in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses'])
651
+ st.write(f"{t['total_analyses']}: {total_analyses}")
652
+
653
+ # Gráfico de tipos de análisis
654
+ analysis_counts = {
655
+ t['morphological_analysis']: len(student_data['morphosyntax_analyses']),
656
+ t['semantic_analyses']: len(student_data['semantic_analyses']),
657
+ t['discourse_analyses']: len(student_data['discourse_analyses'])
658
+ }
659
+ fig, ax = plt.subplots()
660
+ ax.bar(analysis_counts.keys(), analysis_counts.values())
661
+ ax.set_title(t['analysis_types_chart'])
662
+ st.pyplot(fig)
663
+
664
+ # Mostrar los últimos análisis
665
+ for analysis_type in ['morphosyntax_analyses', 'semantic_analyses', 'discourse_analyses']:
666
+ with st.expander(t[f'{analysis_type}_expander']):
667
+ for analysis in student_data[analysis_type][:5]: # Mostrar los últimos 5
668
+ st.subheader(f"{t['analysis_from']} {analysis['timestamp']}")
669
+ if analysis_type == 'morphosyntax_analyses':
670
+ if 'arc_diagrams' in analysis:
671
+ st.write(analysis['arc_diagrams'][0], unsafe_allow_html=True)
672
+ elif analysis_type == 'semantic_analyses':
673
+ if 'key_concepts' in analysis:
674
+ st.write(t['key_concepts'])
675
+ st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis['key_concepts']]))
676
+ if 'graph' in analysis:
677
+ st.image(base64.b64decode(analysis['graph']))
678
+ elif analysis_type == 'discourse_analyses':
679
+ for i in [1, 2]:
680
+ if f'key_concepts{i}' in analysis:
681
+ st.write(f"{t['key_concepts']} {t['document']} {i}")
682
+ st.write(", ".join([f"{concept} ({freq:.2f})" for concept, freq in analysis[f'key_concepts{i}']]))
683
+ if 'combined_graph' in analysis:
684
+ st.image(base64.b64decode(analysis['combined_graph']))
685
+
686
+ # Mostrar el historial de chat
687
+ with st.expander(t['chat_history_expander']):
688
+ for chat in student_data['chat_history'][:5]: # Mostrar las últimas 5 conversaciones
689
+ st.subheader(f"{t['chat_from']} {chat['timestamp']}")
690
+ for message in chat['messages']:
691
+ st.write(f"{message['role'].capitalize()}: {message['content']}")
692
+ st.write("---")
693
+
694
+
695
+
696
+
697
+ def display_student_progress(username, lang_code, t):
698
+ st.subheader(t['student_activities'])
699
+ st.write(f"{t['activities_message']} {username}")
700
+
701
+ # Aquí puedes agregar más contenido estático o placeholder
702
+ st.info(t['activities_placeholder'])
703
+
704
+ # Si necesitas mostrar algún dato, puedes usar datos de ejemplo o placeholders
705
+ col1, col2, col3 = st.columns(3)
706
+ col1.metric(t['morpho_analyses'], "5") # Ejemplo de dato
707
+ col2.metric(t['semantic_analyses'], "3") # Ejemplo de dato
708
+ col3.metric(t['discourse_analyses'], "2") # Ejemplo de dato
709
+
710
+
711
+
712
+ def display_student_progress(username, lang_code, t):
713
+ st.title(f"Actividades de {username}")
714
+
715
+ # Obtener todos los datos del estudiante
716
+ student_data = get_student_data(username)
717
+
718
+ if not student_data or len(student_data.get('entries', [])) == 0:
719
+ st.warning("No se encontraron datos de análisis para este estudiante.")
720
+ st.info("Intenta realizar algunos análisis de texto primero.")
721
+ return
722
+
723
+ # Resumen de actividades
724
+ with st.expander("Resumen de Actividades", expanded=True):
725
+ total_entries = len(student_data['entries'])
726
+ st.write(f"Total de análisis realizados: {total_entries}")
727
+
728
+ # Gráfico de tipos de análisis
729
+ analysis_types = [entry['analysis_type'] for entry in student_data['entries']]
730
+ analysis_counts = pd.Series(analysis_types).value_counts()
731
+ fig, ax = plt.subplots()
732
+ analysis_counts.plot(kind='bar', ax=ax)
733
+ ax.set_title("Tipos de análisis realizados")
734
+ ax.set_xlabel("Tipo de análisis")
735
+ ax.set_ylabel("Cantidad")
736
+ st.pyplot(fig)
737
+
738
+ # Histórico de Análisis Morfosintácticos
739
+ with st.expander("Histórico de Análisis Morfosintácticos"):
740
+ morpho_analyses = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'morphosyntax']
741
+ for analysis in morpho_analyses[:5]: # Mostrar los últimos 5
742
+ st.subheader(f"Análisis del {analysis['timestamp']}")
743
+ if 'arc_diagrams' in analysis:
744
+ st.write(analysis['arc_diagrams'][0], unsafe_allow_html=True)
745
+
746
+ # Histórico de Análisis Semánticos
747
+ with st.expander("Histórico de Análisis Semánticos"):
748
+ semantic_analyses = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'semantic']
749
+ for analysis in semantic_analyses[:5]: # Mostrar los últimos 5
750
+ st.subheader(f"Análisis del {analysis['timestamp']}")
751
+ if 'key_concepts' in analysis:
752
+ concepts_str = " | ".join([f"{concept} ({frequency:.2f})" for concept, frequency in analysis['key_concepts']])
753
+ st.markdown(f"<div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px;'>{concepts_str}</div>", unsafe_allow_html=True)
754
+ if 'graph' in analysis:
755
+ try:
756
+ img_bytes = base64.b64decode(analysis['graph'])
757
+ st.image(img_bytes, caption="Gráfico de relaciones conceptuales")
758
+ except Exception as e:
759
+ st.error(f"No se pudo mostrar el gráfico: {str(e)}")
760
+
761
+ # Histórico de Análisis Discursivos
762
+ with st.expander("Histórico de Análisis Discursivos"):
763
+ discourse_analyses = [entry for entry in student_data['entries'] if entry['analysis_type'] == 'discourse']
764
+ for analysis in discourse_analyses[:5]: # Mostrar los últimos 5
765
+ st.subheader(f"Análisis del {analysis['timestamp']}")
766
+ for i in [1, 2]:
767
+ if f'key_concepts{i}' in analysis:
768
+ concepts_str = " | ".join([f"{concept} ({frequency:.2f})" for concept, frequency in analysis[f'key_concepts{i}']])
769
+ st.write(f"Conceptos clave del documento {i}:")
770
+ st.markdown(f"<div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px;'>{concepts_str}</div>", unsafe_allow_html=True)
771
+ if 'combined_graph' in analysis:
772
+ try:
773
+ img_bytes = base64.b64decode(analysis['combined_graph'])
774
+ st.image(img_bytes)
775
+ except Exception as e:
776
+ st.error(f"No se pudo mostrar el gráfico combinado: {str(e)}")
777
+
778
+ # Histórico de Conversaciones con el ChatBot
779
+ with st.expander("Histórico de Conversaciones con el ChatBot"):
780
+ if 'chat_history' in student_data:
781
+ for i, chat in enumerate(student_data['chat_history'][:5]): # Mostrar las últimas 5 conversaciones
782
+ st.subheader(f"Conversación {i+1} - {chat['timestamp']}")
783
+ for message in chat['messages']:
784
+ st.write(f"{message['role'].capitalize()}: {message['content']}")
785
+ st.write("---")
786
+ else:
787
+ st.write("No se encontraron conversaciones con el ChatBot.")
788
+
789
+ # Opción para mostrar datos de depuración
790
+ if st.checkbox("Mostrar datos de depuración"):
791
+ st.write("Datos del estudiante (para depuración):")
792
+ st.json(student_data)
793
+
794
  '''
modules/studentact/student_activities_v2-error.py CHANGED
@@ -1,251 +1,251 @@
1
- ##############
2
- ###modules/studentact/student_activities_v2.py
3
-
4
- import streamlit as st
5
- import re
6
- import io
7
- from io import BytesIO
8
- import pandas as pd
9
- import numpy as np
10
- import time
11
- import matplotlib.pyplot as plt
12
- from datetime import datetime
13
- from spacy import displacy
14
- import random
15
- import base64
16
- import seaborn as sns
17
- import logging
18
-
19
- # Importaciones de la base de datos
20
- from ..database.morphosintax_mongo_db import get_student_morphosyntax_analysis
21
- from ..database.semantic_mongo_db import get_student_semantic_analysis
22
- from ..database.discourse_mongo_db import get_student_discourse_analysis
23
- from ..database.chat_mongo_db import get_chat_history
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
- ###################################################################################
28
- def display_student_activities(username: str, lang_code: str, t: dict):
29
- """
30
- Muestra todas las actividades del estudiante
31
- Args:
32
- username: Nombre del estudiante
33
- lang_code: Código del idioma
34
- t: Diccionario de traducciones
35
- """
36
- try:
37
- st.header(t.get('activities_title', 'Mis Actividades'))
38
-
39
- # Tabs para diferentes tipos de análisis
40
- tabs = st.tabs([
41
- t.get('morpho_activities', 'Análisis Morfosintáctico'),
42
- t.get('semantic_activities', 'Análisis Semántico'),
43
- t.get('discourse_activities', 'Análisis del Discurso'),
44
- t.get('chat_activities', 'Conversaciones con el Asistente')
45
- ])
46
-
47
- # Tab de Análisis Morfosintáctico
48
- with tabs[0]:
49
- display_morphosyntax_activities(username, t)
50
-
51
- # Tab de Análisis Semántico
52
- with tabs[1]:
53
- display_semantic_activities(username, t)
54
-
55
- # Tab de Análisis del Discurso
56
- with tabs[2]:
57
- display_discourse_activities(username, t)
58
-
59
- # Tab de Conversaciones del Chat
60
- with tabs[3]:
61
- display_chat_activities(username, t)
62
-
63
- except Exception as e:
64
- logger.error(f"Error mostrando actividades: {str(e)}")
65
- st.error(t.get('error_loading_activities', 'Error al cargar las actividades'))
66
-
67
- ###################################################################################
68
- def display_morphosyntax_activities(username: str, t: dict):
69
- """Muestra actividades de análisis morfosintáctico"""
70
- try:
71
- analyses = get_student_morphosyntax_analysis(username)
72
- if not analyses:
73
- st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
74
- return
75
-
76
- for analysis in analyses:
77
- with st.expander(
78
- f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
79
- expanded=False
80
- ):
81
- st.text(f"{t.get('analyzed_text', 'Texto analizado')}:")
82
- st.write(analysis['text'])
83
-
84
- if 'arc_diagrams' in analysis:
85
- st.subheader(t.get('syntactic_diagrams', 'Diagramas sintácticos'))
86
- for diagram in analysis['arc_diagrams']:
87
- st.write(diagram, unsafe_allow_html=True)
88
-
89
- except Exception as e:
90
- logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
91
- st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
92
-
93
- ###################################################################################
94
- def display_semantic_activities(username: str, t: dict):
95
- """Muestra actividades de análisis semántico"""
96
- try:
97
- analyses = get_student_semantic_analysis(username)
98
- if not analyses:
99
- st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
100
- return
101
-
102
- for analysis in analyses:
103
- with st.expander(
104
- f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
105
- expanded=False
106
- ):
107
-
108
- # Mostrar conceptos clave
109
- if 'key_concepts' in analysis:
110
- st.subheader(t.get('key_concepts', 'Conceptos clave'))
111
- df = pd.DataFrame(
112
- analysis['key_concepts'],
113
- columns=['Concepto', 'Frecuencia']
114
- )
115
- st.dataframe(df)
116
-
117
- # Mostrar gráfico de conceptos
118
- if 'concept_graph' in analysis and analysis['concept_graph']:
119
- st.subheader(t.get('concept_graph', 'Grafo de conceptos'))
120
- image_bytes = base64.b64decode(analysis['concept_graph'])
121
- st.image(image_bytes)
122
-
123
- except Exception as e:
124
- logger.error(f"Error mostrando análisis semántico: {str(e)}")
125
- st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
126
-
127
- ###################################################################################
128
-
129
- def display_discourse_activities(username: str, t: dict):
130
- """Muestra actividades de análisis del discurso"""
131
- try:
132
- analyses = get_student_discourse_analysis(username)
133
- if not analyses:
134
- st.info(t.get('no_discourse_analyses', 'No hay análisis del discurso registrados'))
135
- return
136
-
137
- for analysis in analyses:
138
- with st.expander(
139
- f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
140
- expanded=False
141
- ):
142
-
143
- # Mostrar conceptos clave
144
- if 'key_concepts1' in analysis and 'key_concepts2' in analysis:
145
- st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
146
-
147
- col1, col2 = st.columns(2)
148
- with col1:
149
- st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
150
- df1 = pd.DataFrame(
151
- analysis['key_concepts1'],
152
- columns=['Concepto', 'Frecuencia']
153
- )
154
- st.dataframe(df1)
155
-
156
- with col2:
157
- st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
158
- df2 = pd.DataFrame(
159
- analysis['key_concepts2'],
160
- columns=['Concepto', 'Frecuencia']
161
- )
162
- st.dataframe(df2)
163
-
164
- # Mostrar gráficos
165
- if all(key in analysis for key in ['graph1', 'graph2']):
166
- st.subheader(t.get('visualizations', 'Visualizaciones'))
167
-
168
- col1, col2 = st.columns(2)
169
- with col1:
170
- st.markdown(f"**{t.get('graph_text_1', 'Grafo Texto 1')}**")
171
- if analysis['graph1']:
172
- image_bytes = base64.b64decode(analysis['graph1'])
173
- st.image(image_bytes)
174
-
175
- with col2:
176
- st.markdown(f"**{t.get('graph_text_2', 'Grafo Texto 2')}**")
177
- if analysis['graph2']:
178
- image_bytes = base64.b64decode(analysis['graph2'])
179
- st.image(image_bytes)
180
-
181
- except Exception as e:
182
- logger.error(f"Error mostrando análisis del discurso: {str(e)}")
183
- st.error(t.get('error_discourse', 'Error al mostrar análisis del discurso'))
184
- #################################################################################
185
-
186
- def display_discourse_comparison(analysis: dict, t: dict):
187
- """Muestra la comparación de análisis del discurso"""
188
- st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
189
-
190
- col1, col2 = st.columns(2)
191
- with col1:
192
- st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
193
- df1 = pd.DataFrame(analysis['key_concepts1'])
194
- st.dataframe(df1)
195
-
196
- with col2:
197
- st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
198
- df2 = pd.DataFrame(analysis['key_concepts2'])
199
- st.dataframe(df2)
200
-
201
- #################################################################################
202
-
203
-
204
- def display_chat_activities(username: str, t: dict):
205
- """
206
- Muestra historial de conversaciones del chat
207
- """
208
- try:
209
- # Obtener historial del chat
210
- chat_history = get_chat_history(
211
- username=username,
212
- analysis_type='sidebar',
213
- limit=50
214
- )
215
-
216
- if not chat_history:
217
- st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
218
- return
219
-
220
- for chat in reversed(chat_history): # Mostrar las más recientes primero
221
- try:
222
- # Convertir timestamp a datetime para formato
223
- timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
224
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
225
-
226
- with st.expander(
227
- f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
228
- expanded=False
229
- ):
230
- if 'messages' in chat and chat['messages']:
231
- # Mostrar cada mensaje en la conversación
232
- for message in chat['messages']:
233
- role = message.get('role', 'unknown')
234
- content = message.get('content', '')
235
-
236
- # Usar el componente de chat de Streamlit
237
- with st.chat_message(role):
238
- st.markdown(content)
239
-
240
- # Agregar separador entre mensajes
241
- st.divider()
242
- else:
243
- st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
244
-
245
- except Exception as e:
246
- logger.error(f"Error mostrando conversación: {str(e)}")
247
- continue
248
-
249
- except Exception as e:
250
- logger.error(f"Error mostrando historial del chat: {str(e)}")
251
  st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
 
1
+ ##############
2
+ ###modules/studentact/student_activities_v2.py
3
+
4
+ import streamlit as st
5
+ import re
6
+ import io
7
+ from io import BytesIO
8
+ import pandas as pd
9
+ import numpy as np
10
+ import time
11
+ import matplotlib.pyplot as plt
12
+ from datetime import datetime
13
+ from spacy import displacy
14
+ import random
15
+ import base64
16
+ import seaborn as sns
17
+ import logging
18
+
19
+ # Importaciones de la base de datos
20
+ from ..database.morphosintax_mongo_db import get_student_morphosyntax_analysis
21
+ from ..database.semantic_mongo_db import get_student_semantic_analysis
22
+ from ..database.discourse_mongo_db import get_student_discourse_analysis
23
+ from ..database.chat_mongo_db import get_chat_history
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ ###################################################################################
28
+ def display_student_activities(username: str, lang_code: str, t: dict):
29
+ """
30
+ Muestra todas las actividades del estudiante
31
+ Args:
32
+ username: Nombre del estudiante
33
+ lang_code: Código del idioma
34
+ t: Diccionario de traducciones
35
+ """
36
+ try:
37
+ st.header(t.get('activities_title', 'Mis Actividades'))
38
+
39
+ # Tabs para diferentes tipos de análisis
40
+ tabs = st.tabs([
41
+ t.get('morpho_activities', 'Análisis Morfosintáctico'),
42
+ t.get('semantic_activities', 'Análisis Semántico'),
43
+ t.get('discourse_activities', 'Análisis del Discurso'),
44
+ t.get('chat_activities', 'Conversaciones con el Asistente')
45
+ ])
46
+
47
+ # Tab de Análisis Morfosintáctico
48
+ with tabs[0]:
49
+ display_morphosyntax_activities(username, t)
50
+
51
+ # Tab de Análisis Semántico
52
+ with tabs[1]:
53
+ display_semantic_activities(username, t)
54
+
55
+ # Tab de Análisis del Discurso
56
+ with tabs[2]:
57
+ display_discourse_activities(username, t)
58
+
59
+ # Tab de Conversaciones del Chat
60
+ with tabs[3]:
61
+ display_chat_activities(username, t)
62
+
63
+ except Exception as e:
64
+ logger.error(f"Error mostrando actividades: {str(e)}")
65
+ st.error(t.get('error_loading_activities', 'Error al cargar las actividades'))
66
+
67
+ ###################################################################################
68
+ def display_morphosyntax_activities(username: str, t: dict):
69
+ """Muestra actividades de análisis morfosintáctico"""
70
+ try:
71
+ analyses = get_student_morphosyntax_analysis(username)
72
+ if not analyses:
73
+ st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
74
+ return
75
+
76
+ for analysis in analyses:
77
+ with st.expander(
78
+ f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
79
+ expanded=False
80
+ ):
81
+ st.text(f"{t.get('analyzed_text', 'Texto analizado')}:")
82
+ st.write(analysis['text'])
83
+
84
+ if 'arc_diagrams' in analysis:
85
+ st.subheader(t.get('syntactic_diagrams', 'Diagramas sintácticos'))
86
+ for diagram in analysis['arc_diagrams']:
87
+ st.write(diagram, unsafe_allow_html=True)
88
+
89
+ except Exception as e:
90
+ logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
91
+ st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
92
+
93
+ ###################################################################################
94
+ def display_semantic_activities(username: str, t: dict):
95
+ """Muestra actividades de análisis semántico"""
96
+ try:
97
+ analyses = get_student_semantic_analysis(username)
98
+ if not analyses:
99
+ st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
100
+ return
101
+
102
+ for analysis in analyses:
103
+ with st.expander(
104
+ f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
105
+ expanded=False
106
+ ):
107
+
108
+ # Mostrar conceptos clave
109
+ if 'key_concepts' in analysis:
110
+ st.subheader(t.get('key_concepts', 'Conceptos clave'))
111
+ df = pd.DataFrame(
112
+ analysis['key_concepts'],
113
+ columns=['Concepto', 'Frecuencia']
114
+ )
115
+ st.dataframe(df)
116
+
117
+ # Mostrar gráfico de conceptos
118
+ if 'concept_graph' in analysis and analysis['concept_graph']:
119
+ st.subheader(t.get('concept_graph', 'Grafo de conceptos'))
120
+ image_bytes = base64.b64decode(analysis['concept_graph'])
121
+ st.image(image_bytes)
122
+
123
+ except Exception as e:
124
+ logger.error(f"Error mostrando análisis semántico: {str(e)}")
125
+ st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
126
+
127
+ ###################################################################################
128
+
129
+ def display_discourse_activities(username: str, t: dict):
130
+ """Muestra actividades de análisis del discurso"""
131
+ try:
132
+ analyses = get_student_discourse_analysis(username)
133
+ if not analyses:
134
+ st.info(t.get('no_discourse_analyses', 'No hay análisis del discurso registrados'))
135
+ return
136
+
137
+ for analysis in analyses:
138
+ with st.expander(
139
+ f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
140
+ expanded=False
141
+ ):
142
+
143
+ # Mostrar conceptos clave
144
+ if 'key_concepts1' in analysis and 'key_concepts2' in analysis:
145
+ st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
146
+
147
+ col1, col2 = st.columns(2)
148
+ with col1:
149
+ st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
150
+ df1 = pd.DataFrame(
151
+ analysis['key_concepts1'],
152
+ columns=['Concepto', 'Frecuencia']
153
+ )
154
+ st.dataframe(df1)
155
+
156
+ with col2:
157
+ st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
158
+ df2 = pd.DataFrame(
159
+ analysis['key_concepts2'],
160
+ columns=['Concepto', 'Frecuencia']
161
+ )
162
+ st.dataframe(df2)
163
+
164
+ # Mostrar gráficos
165
+ if all(key in analysis for key in ['graph1', 'graph2']):
166
+ st.subheader(t.get('visualizations', 'Visualizaciones'))
167
+
168
+ col1, col2 = st.columns(2)
169
+ with col1:
170
+ st.markdown(f"**{t.get('graph_text_1', 'Grafo Texto 1')}**")
171
+ if analysis['graph1']:
172
+ image_bytes = base64.b64decode(analysis['graph1'])
173
+ st.image(image_bytes)
174
+
175
+ with col2:
176
+ st.markdown(f"**{t.get('graph_text_2', 'Grafo Texto 2')}**")
177
+ if analysis['graph2']:
178
+ image_bytes = base64.b64decode(analysis['graph2'])
179
+ st.image(image_bytes)
180
+
181
+ except Exception as e:
182
+ logger.error(f"Error mostrando análisis del discurso: {str(e)}")
183
+ st.error(t.get('error_discourse', 'Error al mostrar análisis del discurso'))
184
+ #################################################################################
185
+
186
+ def display_discourse_comparison(analysis: dict, t: dict):
187
+ """Muestra la comparación de análisis del discurso"""
188
+ st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
189
+
190
+ col1, col2 = st.columns(2)
191
+ with col1:
192
+ st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
193
+ df1 = pd.DataFrame(analysis['key_concepts1'])
194
+ st.dataframe(df1)
195
+
196
+ with col2:
197
+ st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
198
+ df2 = pd.DataFrame(analysis['key_concepts2'])
199
+ st.dataframe(df2)
200
+
201
+ #################################################################################
202
+
203
+
204
+ def display_chat_activities(username: str, t: dict):
205
+ """
206
+ Muestra historial de conversaciones del chat
207
+ """
208
+ try:
209
+ # Obtener historial del chat
210
+ chat_history = get_chat_history(
211
+ username=username,
212
+ analysis_type='sidebar',
213
+ limit=50
214
+ )
215
+
216
+ if not chat_history:
217
+ st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
218
+ return
219
+
220
+ for chat in reversed(chat_history): # Mostrar las más recientes primero
221
+ try:
222
+ # Convertir timestamp a datetime para formato
223
+ timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
224
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
225
+
226
+ with st.expander(
227
+ f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
228
+ expanded=False
229
+ ):
230
+ if 'messages' in chat and chat['messages']:
231
+ # Mostrar cada mensaje en la conversación
232
+ for message in chat['messages']:
233
+ role = message.get('role', 'unknown')
234
+ content = message.get('content', '')
235
+
236
+ # Usar el componente de chat de Streamlit
237
+ with st.chat_message(role):
238
+ st.markdown(content)
239
+
240
+ # Agregar separador entre mensajes
241
+ st.divider()
242
+ else:
243
+ st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
244
+
245
+ except Exception as e:
246
+ logger.error(f"Error mostrando conversación: {str(e)}")
247
+ continue
248
+
249
+ except Exception as e:
250
+ logger.error(f"Error mostrando historial del chat: {str(e)}")
251
  st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
modules/studentact/student_activities_v2.py CHANGED
@@ -1,282 +1,571 @@
1
- ##############
2
- ###modules/studentact/student_activities_v2.py
3
-
4
- import streamlit as st
5
- import re
6
- import io
7
- from io import BytesIO
8
- import pandas as pd
9
- import numpy as np
10
- import time
11
- import matplotlib.pyplot as plt
12
- from datetime import datetime
13
- from spacy import displacy
14
- import random
15
- import base64
16
- import seaborn as sns
17
- import logging
18
-
19
- # Importaciones de la base de datos
20
- from ..database.morphosintax_mongo_db import get_student_morphosyntax_analysis
21
- from ..database.semantic_mongo_db import get_student_semantic_analysis
22
- from ..database.discourse_mongo_db import get_student_discourse_analysis
23
- from ..database.chat_mongo_db import get_chat_history
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
- ###################################################################################
28
-
29
- def display_student_activities(username: str, lang_code: str, t: dict):
30
- """
31
- Muestra todas las actividades del estudiante
32
- Args:
33
- username: Nombre del estudiante
34
- lang_code: Código del idioma
35
- t: Diccionario de traducciones
36
- """
37
- try:
38
- st.header(t.get('activities_title', 'Mis Actividades'))
39
-
40
- # Tabs para diferentes tipos de análisis
41
- tabs = st.tabs([
42
- t.get('morpho_activities', 'Análisis Morfosintáctico'),
43
- t.get('semantic_activities', 'Análisis Semántico'),
44
- t.get('discourse_activities', 'Análisis del Discurso'),
45
- t.get('chat_activities', 'Conversaciones con el Asistente')
46
- ])
47
-
48
- # Tab de Análisis Morfosintáctico
49
- with tabs[0]:
50
- display_morphosyntax_activities(username, t)
51
-
52
- # Tab de Análisis Semántico
53
- with tabs[1]:
54
- display_semantic_activities(username, t)
55
-
56
- # Tab de Análisis del Discurso
57
- with tabs[2]:
58
- display_discourse_activities(username, t)
59
-
60
- # Tab de Conversaciones del Chat
61
- with tabs[3]:
62
- display_chat_activities(username, t)
63
-
64
- except Exception as e:
65
- logger.error(f"Error mostrando actividades: {str(e)}")
66
- st.error(t.get('error_loading_activities', 'Error al cargar las actividades'))
67
-
68
-
69
- ###############################################################################################
70
- def display_morphosyntax_activities(username: str, t: dict):
71
- """Muestra actividades de análisis morfosintáctico"""
72
- try:
73
- analyses = get_student_morphosyntax_analysis(username)
74
- if not analyses:
75
- st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
76
- return
77
-
78
- for analysis in analyses:
79
- with st.expander(
80
- f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
81
- expanded=False
82
- ):
83
- st.text(f"{t.get('analyzed_text', 'Texto analizado')}:")
84
- st.write(analysis['text'])
85
-
86
- if 'arc_diagrams' in analysis:
87
- st.subheader(t.get('syntactic_diagrams', 'Diagramas sintácticos'))
88
- for diagram in analysis['arc_diagrams']:
89
- st.write(diagram, unsafe_allow_html=True)
90
-
91
- except Exception as e:
92
- logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
93
- st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
94
-
95
-
96
- ###############################################################################################
97
-
98
- def display_semantic_activities(username: str, t: dict):
99
- """Muestra actividades de análisis semántico"""
100
- try:
101
- logger.info(f"Recuperando análisis semántico para {username}")
102
- analyses = get_student_semantic_analysis(username)
103
-
104
- if not analyses:
105
- logger.info("No se encontraron análisis semánticos")
106
- st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
107
- return
108
-
109
- logger.info(f"Procesando {len(analyses)} análisis semánticos")
110
-
111
- for analysis in analyses:
112
- try:
113
- # Verificar campos necesarios
114
- if not all(key in analysis for key in ['timestamp', 'concept_graph']):
115
- logger.warning(f"Análisis incompleto: {analysis.keys()}")
116
- continue
117
-
118
- # Formatear fecha
119
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
120
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
121
-
122
- # Crear expander
123
- with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
124
- # Procesar y mostrar gráfico
125
- if analysis.get('concept_graph'):
126
- try:
127
- # Convertir de base64 a bytes
128
- logger.debug("Decodificando gráfico de conceptos")
129
- image_data = analysis['concept_graph']
130
-
131
- # Si el gráfico ya es bytes, usarlo directamente
132
- if isinstance(image_data, bytes):
133
- image_bytes = image_data
134
- else:
135
- # Si es string base64, decodificar
136
- image_bytes = base64.b64decode(image_data)
137
-
138
- logger.debug(f"Longitud de bytes de imagen: {len(image_bytes)}")
139
-
140
- # Mostrar imagen
141
- st.image(
142
- image_bytes,
143
- caption=t.get('concept_network', 'Red de Conceptos'),
144
- use_column_width=True
145
- )
146
- logger.debug("Gráfico mostrado exitosamente")
147
-
148
- except Exception as img_error:
149
- logger.error(f"Error procesando gráfico: {str(img_error)}")
150
- st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
151
- else:
152
- st.info(t.get('no_graph', 'No hay visualización disponible'))
153
-
154
- except Exception as e:
155
- logger.error(f"Error procesando análisis individual: {str(e)}")
156
- continue
157
-
158
- except Exception as e:
159
- logger.error(f"Error mostrando análisis semántico: {str(e)}")
160
- st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
- ###################################################################################################
174
- def display_discourse_activities(username: str, t: dict):
175
- """Muestra actividades de análisis del discurso"""
176
- try:
177
- logger.info(f"Recuperando análisis del discurso para {username}")
178
- analyses = get_student_discourse_analysis(username)
179
-
180
- if not analyses:
181
- logger.info("No se encontraron análisis del discurso")
182
- st.info(t.get('no_discourse_analyses', 'No hay análisis del discurso registrados'))
183
- return
184
-
185
- logger.info(f"Procesando {len(analyses)} análisis del discurso")
186
- for analysis in analyses:
187
- try:
188
- # Verificar campos mínimos necesarios
189
- if not all(key in analysis for key in ['timestamp', 'combined_graph']):
190
- logger.warning(f"Análisis incompleto: {analysis.keys()}")
191
- continue
192
-
193
- # Formatear fecha
194
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
195
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
196
-
197
- with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
198
- if analysis['combined_graph']:
199
- logger.debug("Decodificando gráfico combinado")
200
- try:
201
- image_bytes = base64.b64decode(analysis['combined_graph'])
202
- st.image(image_bytes, use_column_width=True)
203
- logger.debug("Gráfico mostrado exitosamente")
204
- except Exception as img_error:
205
- logger.error(f"Error decodificando imagen: {str(img_error)}")
206
- st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
207
- else:
208
- st.info(t.get('no_visualization', 'No hay visualización comparativa disponible'))
209
-
210
- except Exception as e:
211
- logger.error(f"Error procesando análisis individual: {str(e)}")
212
- continue
213
-
214
- except Exception as e:
215
- logger.error(f"Error mostrando análisis del discurso: {str(e)}")
216
- st.error(t.get('error_discourse', 'Error al mostrar análisis del discurso'))
217
-
218
- #################################################################################
219
- def display_discourse_comparison(analysis: dict, t: dict):
220
- """Muestra la comparación de análisis del discurso"""
221
- st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
222
-
223
- col1, col2 = st.columns(2)
224
- with col1:
225
- st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
226
- df1 = pd.DataFrame(analysis['key_concepts1'])
227
- st.dataframe(df1)
228
-
229
- with col2:
230
- st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
231
- df2 = pd.DataFrame(analysis['key_concepts2'])
232
- st.dataframe(df2)
233
-
234
- #################################################################################
235
- def display_chat_activities(username: str, t: dict):
236
- """
237
- Muestra historial de conversaciones del chat
238
- """
239
- try:
240
- # Obtener historial del chat
241
- chat_history = get_chat_history(
242
- username=username,
243
- analysis_type='sidebar',
244
- limit=50
245
- )
246
-
247
- if not chat_history:
248
- st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
249
- return
250
-
251
- for chat in reversed(chat_history): # Mostrar las más recientes primero
252
- try:
253
- # Convertir timestamp a datetime para formato
254
- timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
255
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
256
-
257
- with st.expander(
258
- f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
259
- expanded=False
260
- ):
261
- if 'messages' in chat and chat['messages']:
262
- # Mostrar cada mensaje en la conversación
263
- for message in chat['messages']:
264
- role = message.get('role', 'unknown')
265
- content = message.get('content', '')
266
-
267
- # Usar el componente de chat de Streamlit
268
- with st.chat_message(role):
269
- st.markdown(content)
270
-
271
- # Agregar separador entre mensajes
272
- st.divider()
273
- else:
274
- st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
275
-
276
- except Exception as e:
277
- logger.error(f"Error mostrando conversación: {str(e)}")
278
- continue
279
-
280
- except Exception as e:
281
- logger.error(f"Error mostrando historial del chat: {str(e)}")
282
- st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##############
2
+ ###modules/studentact/student_activities_v2.py
3
+
4
+ import streamlit as st
5
+ import re
6
+ import io
7
+ from io import BytesIO
8
+ import pandas as pd
9
+ import numpy as np
10
+ import time
11
+ import matplotlib.pyplot as plt
12
+ from datetime import datetime, timedelta
13
+ from spacy import displacy
14
+ import random
15
+ import base64
16
+ import seaborn as sns
17
+ import logging
18
+
19
+ # Importaciones de la base de datos
20
+ from ..database.morphosintax_mongo_db import get_student_morphosyntax_analysis
21
+ from ..database.semantic_mongo_db import get_student_semantic_analysis
22
+ from ..database.discourse_mongo_db import get_student_discourse_analysis
23
+ from ..database.chat_mongo_db import get_chat_history
24
+ from ..database.current_situation_mongo_db import get_current_situation_analysis
25
+ from ..database.claude_recommendations_mongo_db import get_claude_recommendations
26
+
27
+ # Importar la función generate_unique_key
28
+ from ..utils.widget_utils import generate_unique_key
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ ###################################################################################
33
+
34
+ def display_student_activities(username: str, lang_code: str, t: dict):
35
+ """
36
+ Muestra todas las actividades del estudiante
37
+ Args:
38
+ username: Nombre del estudiante
39
+ lang_code: Código del idioma
40
+ t: Diccionario de traducciones
41
+ """
42
+ try:
43
+ st.header(t.get('activities_title', 'Mis Actividades'))
44
+
45
+ # Tabs para diferentes tipos de análisis
46
+ tabs = st.tabs([
47
+ t.get('current_situation_activities', 'Mi Situación Actual'),
48
+ t.get('morpho_activities', 'Análisis Morfosintáctico'),
49
+ t.get('semantic_activities', 'Análisis Semántico'),
50
+ t.get('discourse_activities', 'Análisis del Discurso'),
51
+ t.get('chat_activities', 'Conversaciones con el Asistente')
52
+ ])
53
+
54
+ # Tab de Situación Actual
55
+ with tabs[0]:
56
+ display_current_situation_activities(username, t)
57
+
58
+ # Tab de Análisis Morfosintáctico
59
+ with tabs[1]:
60
+ display_morphosyntax_activities(username, t)
61
+
62
+ # Tab de Análisis Semántico
63
+ with tabs[2]:
64
+ display_semantic_activities(username, t)
65
+
66
+ # Tab de Análisis del Discurso
67
+ with tabs[3]:
68
+ display_discourse_activities(username, t)
69
+
70
+ # Tab de Conversaciones del Chat
71
+ with tabs[4]:
72
+ display_chat_activities(username, t)
73
+
74
+ except Exception as e:
75
+ logger.error(f"Error mostrando actividades: {str(e)}")
76
+ st.error(t.get('error_loading_activities', 'Error al cargar las actividades'))
77
+
78
+
79
+ ###############################################################################################
80
+
81
+ def display_current_situation_activities(username: str, t: dict):
82
+ """
83
+ Muestra análisis de situación actual junto con las recomendaciones de Claude
84
+ unificando la información de ambas colecciones y emparejándolas por cercanía temporal.
85
+ """
86
+ try:
87
+ # Recuperar datos de ambas colecciones
88
+ logger.info(f"Recuperando análisis de situación actual para {username}")
89
+ situation_analyses = get_current_situation_analysis(username, limit=10)
90
+
91
+ # Verificar si hay datos
92
+ if situation_analyses:
93
+ logger.info(f"Recuperados {len(situation_analyses)} análisis de situación")
94
+ # Depurar para ver la estructura de datos
95
+ for i, analysis in enumerate(situation_analyses):
96
+ logger.info(f"Análisis #{i+1}: Claves disponibles: {list(analysis.keys())}")
97
+ if 'metrics' in analysis:
98
+ logger.info(f"Métricas disponibles: {list(analysis['metrics'].keys())}")
99
+ else:
100
+ logger.warning("No se encontraron análisis de situación actual")
101
+
102
+ logger.info(f"Recuperando recomendaciones de Claude para {username}")
103
+ claude_recommendations = get_claude_recommendations(username)
104
+
105
+ if claude_recommendations:
106
+ logger.info(f"Recuperadas {len(claude_recommendations)} recomendaciones de Claude")
107
+ else:
108
+ logger.warning("No se encontraron recomendaciones de Claude")
109
+
110
+ # Verificar si hay algún tipo de análisis disponible
111
+ if not situation_analyses and not claude_recommendations:
112
+ logger.info("No se encontraron análisis de situación actual ni recomendaciones")
113
+ st.info(t.get('no_current_situation', 'No hay análisis de situación actual registrados'))
114
+ return
115
+
116
+ # Crear pares combinados emparejando diagnósticos y recomendaciones cercanos en tiempo
117
+ logger.info("Creando emparejamientos temporales de análisis")
118
+
119
+ # Convertir timestamps a objetos datetime para comparación
120
+ situation_times = []
121
+ for analysis in situation_analyses:
122
+ if 'timestamp' in analysis:
123
+ try:
124
+ timestamp_str = analysis['timestamp']
125
+ dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
126
+ situation_times.append((dt, analysis))
127
+ except Exception as e:
128
+ logger.error(f"Error parseando timestamp de situación: {str(e)}")
129
+
130
+ recommendation_times = []
131
+ for recommendation in claude_recommendations:
132
+ if 'timestamp' in recommendation:
133
+ try:
134
+ timestamp_str = recommendation['timestamp']
135
+ dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
136
+ recommendation_times.append((dt, recommendation))
137
+ except Exception as e:
138
+ logger.error(f"Error parseando timestamp de recomendación: {str(e)}")
139
+
140
+ # Ordenar por tiempo
141
+ situation_times.sort(key=lambda x: x[0], reverse=True)
142
+ recommendation_times.sort(key=lambda x: x[0], reverse=True)
143
+
144
+ # Crear pares combinados
145
+ combined_items = []
146
+
147
+ # Primero, procesar todas las situaciones encontrando la recomendación más cercana
148
+ for sit_time, situation in situation_times:
149
+ # Buscar la recomendación más cercana en tiempo
150
+ best_match = None
151
+ min_diff = timedelta(minutes=30) # Máxima diferencia de tiempo aceptable (30 minutos)
152
+ best_rec_time = None
153
+
154
+ for rec_time, recommendation in recommendation_times:
155
+ time_diff = abs(sit_time - rec_time)
156
+ if time_diff < min_diff:
157
+ min_diff = time_diff
158
+ best_match = recommendation
159
+ best_rec_time = rec_time
160
+
161
+ # Crear un elemento combinado
162
+ if best_match:
163
+ timestamp_key = sit_time.isoformat()
164
+ combined_items.append((timestamp_key, {
165
+ 'situation': situation,
166
+ 'recommendation': best_match,
167
+ 'time_diff': min_diff.total_seconds()
168
+ }))
169
+ # Eliminar la recomendación usada para no reutilizarla
170
+ recommendation_times = [(t, r) for t, r in recommendation_times if t != best_rec_time]
171
+ logger.info(f"Emparejado: Diagnóstico {sit_time} con Recomendación {best_rec_time} (diferencia: {min_diff})")
172
+ else:
173
+ # Si no hay recomendación cercana, solo incluir la situación
174
+ timestamp_key = sit_time.isoformat()
175
+ combined_items.append((timestamp_key, {
176
+ 'situation': situation
177
+ }))
178
+ logger.info(f"Sin emparejar: Diagnóstico {sit_time} sin recomendación cercana")
179
+
180
+ # Agregar recomendaciones restantes sin situación
181
+ for rec_time, recommendation in recommendation_times:
182
+ timestamp_key = rec_time.isoformat()
183
+ combined_items.append((timestamp_key, {
184
+ 'recommendation': recommendation
185
+ }))
186
+ logger.info(f"Sin emparejar: Recomendación {rec_time} sin diagnóstico cercano")
187
+
188
+ # Ordenar por tiempo (más reciente primero)
189
+ combined_items.sort(key=lambda x: x[0], reverse=True)
190
+
191
+ logger.info(f"Procesando {len(combined_items)} elementos combinados")
192
+
193
+ # Mostrar cada par combinado
194
+ for i, (timestamp_key, analysis_pair) in enumerate(combined_items):
195
+ try:
196
+ # Obtener datos de situación y recomendación
197
+ situation_data = analysis_pair.get('situation', {})
198
+ recommendation_data = analysis_pair.get('recommendation', {})
199
+ time_diff = analysis_pair.get('time_diff')
200
+
201
+ # Si no hay ningún dato, continuar al siguiente
202
+ if not situation_data and not recommendation_data:
203
+ continue
204
+
205
+ # Determinar qué texto mostrar (priorizar el de la situación)
206
+ text_to_show = situation_data.get('text', recommendation_data.get('text', ''))
207
+ text_type = situation_data.get('text_type', recommendation_data.get('text_type', ''))
208
+
209
+ # Formatear fecha para mostrar
210
+ try:
211
+ # Usar timestamp del key que ya es un formato ISO
212
+ dt = datetime.fromisoformat(timestamp_key)
213
+ formatted_date = dt.strftime("%d/%m/%Y %H:%M:%S")
214
+ except Exception as date_error:
215
+ logger.error(f"Error formateando fecha: {str(date_error)}")
216
+ formatted_date = timestamp_key
217
+
218
+ # Determinar el título del expander
219
+ title = f"{t.get('analysis_date', 'Fecha')}: {formatted_date}"
220
+ if text_type:
221
+ text_type_display = {
222
+ 'academic_article': t.get('academic_article', 'Artículo académico'),
223
+ 'student_essay': t.get('student_essay', 'Trabajo universitario'),
224
+ 'general_communication': t.get('general_communication', 'Comunicación general')
225
+ }.get(text_type, text_type)
226
+ title += f" - {text_type_display}"
227
+
228
+ # Añadir indicador de emparejamiento si existe
229
+ if time_diff is not None:
230
+ if time_diff < 60: # menos de un minuto
231
+ title += f" 🔄 (emparejados)"
232
+ else:
233
+ title += f" 🔄 (emparejados, diferencia: {int(time_diff//60)} min)"
234
+
235
+ # Usar un ID único para cada expander
236
+ expander_id = f"analysis_{i}_{timestamp_key.replace(':', '_')}"
237
+
238
+ # Mostrar el análisis en un expander
239
+ with st.expander(title, expanded=False):
240
+ # Mostrar texto analizado con key único
241
+ st.subheader(t.get('analyzed_text', 'Texto analizado'))
242
+ st.text_area(
243
+ "Text Content",
244
+ value=text_to_show,
245
+ height=100,
246
+ disabled=True,
247
+ label_visibility="collapsed",
248
+ key=f"text_area_{expander_id}"
249
+ )
250
+
251
+ # Crear tabs para separar diagnóstico y recomendaciones
252
+ diagnosis_tab, recommendations_tab = st.tabs([
253
+ t.get('diagnosis_tab', 'Diagnóstico'),
254
+ t.get('recommendations_tab', 'Recomendaciones')
255
+ ])
256
+
257
+ # Tab de diagnóstico
258
+ with diagnosis_tab:
259
+ if situation_data and 'metrics' in situation_data:
260
+ metrics = situation_data['metrics']
261
+
262
+ # Dividir en dos columnas
263
+ col1, col2 = st.columns(2)
264
+
265
+ # Principales métricas en formato de tarjetas
266
+ with col1:
267
+ st.subheader(t.get('key_metrics', 'Métricas clave'))
268
+
269
+ # Mostrar cada métrica principal
270
+ for metric_name, metric_data in metrics.items():
271
+ try:
272
+ # Determinar la puntuación
273
+ score = None
274
+ if isinstance(metric_data, dict):
275
+ # Intentar diferentes nombres de campo
276
+ if 'normalized_score' in metric_data:
277
+ score = metric_data['normalized_score']
278
+ elif 'score' in metric_data:
279
+ score = metric_data['score']
280
+ elif 'value' in metric_data:
281
+ score = metric_data['value']
282
+ elif isinstance(metric_data, (int, float)):
283
+ score = metric_data
284
+
285
+ if score is not None:
286
+ # Asegurarse de que score es numérico
287
+ if isinstance(score, (int, float)):
288
+ # Determinar color y emoji basado en la puntuación
289
+ if score < 0.5:
290
+ emoji = "🔴"
291
+ color = "#ffcccc" # light red
292
+ elif score < 0.75:
293
+ emoji = "🟡"
294
+ color = "#ffffcc" # light yellow
295
+ else:
296
+ emoji = "🟢"
297
+ color = "#ccffcc" # light green
298
+
299
+ # Mostrar la métrica con estilo
300
+ st.markdown(f"""
301
+ <div style="background-color:{color}; padding:10px; border-radius:5px; margin-bottom:10px;">
302
+ <b>{emoji} {metric_name.capitalize()}:</b> {score:.2f}
303
+ </div>
304
+ """, unsafe_allow_html=True)
305
+ else:
306
+ # Si no es numérico, mostrar como texto
307
+ st.markdown(f"""
308
+ <div style="background-color:#f0f0f0; padding:10px; border-radius:5px; margin-bottom:10px;">
309
+ <b>ℹ️ {metric_name.capitalize()}:</b> {str(score)}
310
+ </div>
311
+ """, unsafe_allow_html=True)
312
+ except Exception as e:
313
+ logger.error(f"Error procesando métrica {metric_name}: {str(e)}")
314
+
315
+ # Mostrar detalles adicionales si están disponibles
316
+ with col2:
317
+ st.subheader(t.get('details', 'Detalles'))
318
+
319
+ # Para cada métrica, mostrar sus detalles si existen
320
+ for metric_name, metric_data in metrics.items():
321
+ try:
322
+ if isinstance(metric_data, dict):
323
+ # Mostrar detalles directamente o buscar en subcampos
324
+ details = None
325
+ if 'details' in metric_data and metric_data['details']:
326
+ details = metric_data['details']
327
+ else:
328
+ # Crear un diccionario con los detalles excluyendo 'normalized_score' y similares
329
+ details = {k: v for k, v in metric_data.items()
330
+ if k not in ['normalized_score', 'score', 'value']}
331
+
332
+ if details:
333
+ st.write(f"**{metric_name.capitalize()}**")
334
+ st.json(details, expanded=False)
335
+ except Exception as e:
336
+ logger.error(f"Error mostrando detalles de {metric_name}: {str(e)}")
337
+ else:
338
+ st.info(t.get('no_diagnosis', 'No hay datos de diagnóstico disponibles'))
339
+
340
+ # Tab de recomendaciones
341
+ with recommendations_tab:
342
+ if recommendation_data and 'recommendations' in recommendation_data:
343
+ st.markdown(f"""
344
+ <div style="padding: 20px; border-radius: 10px;
345
+ background-color: #f8f9fa; margin-bottom: 20px;">
346
+ {recommendation_data['recommendations']}
347
+ </div>
348
+ """, unsafe_allow_html=True)
349
+ elif recommendation_data and 'feedback' in recommendation_data:
350
+ st.markdown(f"""
351
+ <div style="padding: 20px; border-radius: 10px;
352
+ background-color: #f8f9fa; margin-bottom: 20px;">
353
+ {recommendation_data['feedback']}
354
+ </div>
355
+ """, unsafe_allow_html=True)
356
+ else:
357
+ st.info(t.get('no_recommendations', 'No hay recomendaciones disponibles'))
358
+
359
+ except Exception as e:
360
+ logger.error(f"Error procesando par de análisis: {str(e)}")
361
+ continue
362
+
363
+ except Exception as e:
364
+ logger.error(f"Error mostrando actividades de situación actual: {str(e)}")
365
+ st.error(t.get('error_current_situation', 'Error al mostrar análisis de situación actual'))
366
+
367
+ ###############################################################################################
368
+
369
+ def display_morphosyntax_activities(username: str, t: dict):
370
+ """Muestra actividades de análisis morfosintáctico"""
371
+ try:
372
+ analyses = get_student_morphosyntax_analysis(username)
373
+ if not analyses:
374
+ st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
375
+ return
376
+
377
+ for analysis in analyses:
378
+ with st.expander(
379
+ f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
380
+ expanded=False
381
+ ):
382
+ st.text(f"{t.get('analyzed_text', 'Texto analizado')}:")
383
+ st.write(analysis['text'])
384
+
385
+ if 'arc_diagrams' in analysis:
386
+ st.subheader(t.get('syntactic_diagrams', 'Diagramas sintácticos'))
387
+ for diagram in analysis['arc_diagrams']:
388
+ st.write(diagram, unsafe_allow_html=True)
389
+
390
+ except Exception as e:
391
+ logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
392
+ st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
393
+
394
+
395
+ ###############################################################################################
396
+
397
+ def display_semantic_activities(username: str, t: dict):
398
+ """Muestra actividades de análisis semántico"""
399
+ try:
400
+ logger.info(f"Recuperando análisis semántico para {username}")
401
+ analyses = get_student_semantic_analysis(username)
402
+
403
+ if not analyses:
404
+ logger.info("No se encontraron análisis semánticos")
405
+ st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
406
+ return
407
+
408
+ logger.info(f"Procesando {len(analyses)} análisis semánticos")
409
+
410
+ for analysis in analyses:
411
+ try:
412
+ # Verificar campos necesarios
413
+ if not all(key in analysis for key in ['timestamp', 'concept_graph']):
414
+ logger.warning(f"Análisis incompleto: {analysis.keys()}")
415
+ continue
416
+
417
+ # Formatear fecha
418
+ timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
419
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
420
+
421
+ # Crear expander
422
+ with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
423
+ # Procesar y mostrar gráfico
424
+ if analysis.get('concept_graph'):
425
+ try:
426
+ # Convertir de base64 a bytes
427
+ logger.debug("Decodificando gráfico de conceptos")
428
+ image_data = analysis['concept_graph']
429
+
430
+ # Si el gráfico ya es bytes, usarlo directamente
431
+ if isinstance(image_data, bytes):
432
+ image_bytes = image_data
433
+ else:
434
+ # Si es string base64, decodificar
435
+ image_bytes = base64.b64decode(image_data)
436
+
437
+ logger.debug(f"Longitud de bytes de imagen: {len(image_bytes)}")
438
+
439
+ # Mostrar imagen
440
+ st.image(
441
+ image_bytes,
442
+ caption=t.get('concept_network', 'Red de Conceptos'),
443
+ use_column_width=True
444
+ )
445
+ logger.debug("Gráfico mostrado exitosamente")
446
+
447
+ except Exception as img_error:
448
+ logger.error(f"Error procesando gráfico: {str(img_error)}")
449
+ st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
450
+ else:
451
+ st.info(t.get('no_graph', 'No hay visualización disponible'))
452
+
453
+ except Exception as e:
454
+ logger.error(f"Error procesando análisis individual: {str(e)}")
455
+ continue
456
+
457
+ except Exception as e:
458
+ logger.error(f"Error mostrando análisis semántico: {str(e)}")
459
+ st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
460
+
461
+
462
+ ###################################################################################################
463
+ def display_discourse_activities(username: str, t: dict):
464
+ """Muestra actividades de análisis del discurso"""
465
+ try:
466
+ logger.info(f"Recuperando análisis del discurso para {username}")
467
+ analyses = get_student_discourse_analysis(username)
468
+
469
+ if not analyses:
470
+ logger.info("No se encontraron análisis del discurso")
471
+ st.info(t.get('no_discourse_analyses', 'No hay análisis del discurso registrados'))
472
+ return
473
+
474
+ logger.info(f"Procesando {len(analyses)} análisis del discurso")
475
+ for analysis in analyses:
476
+ try:
477
+ # Verificar campos mínimos necesarios
478
+ if not all(key in analysis for key in ['timestamp', 'combined_graph']):
479
+ logger.warning(f"Análisis incompleto: {analysis.keys()}")
480
+ continue
481
+
482
+ # Formatear fecha
483
+ timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
484
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
485
+
486
+ with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
487
+ if analysis['combined_graph']:
488
+ logger.debug("Decodificando gráfico combinado")
489
+ try:
490
+ image_bytes = base64.b64decode(analysis['combined_graph'])
491
+ st.image(image_bytes, use_column_width=True)
492
+ logger.debug("Gráfico mostrado exitosamente")
493
+ except Exception as img_error:
494
+ logger.error(f"Error decodificando imagen: {str(img_error)}")
495
+ st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
496
+ else:
497
+ st.info(t.get('no_visualization', 'No hay visualización comparativa disponible'))
498
+
499
+ except Exception as e:
500
+ logger.error(f"Error procesando análisis individual: {str(e)}")
501
+ continue
502
+
503
+ except Exception as e:
504
+ logger.error(f"Error mostrando análisis del discurso: {str(e)}")
505
+ st.error(t.get('error_discourse', 'Error al mostrar análisis del discurso'))
506
+
507
+ #################################################################################
508
+ def display_chat_activities(username: str, t: dict):
509
+ """
510
+ Muestra historial de conversaciones del chat
511
+ """
512
+ try:
513
+ # Obtener historial del chat
514
+ chat_history = get_chat_history(
515
+ username=username,
516
+ analysis_type='sidebar',
517
+ limit=50
518
+ )
519
+
520
+ if not chat_history:
521
+ st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
522
+ return
523
+
524
+ for chat in reversed(chat_history): # Mostrar las más recientes primero
525
+ try:
526
+ # Convertir timestamp a datetime para formato
527
+ timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
528
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
529
+
530
+ with st.expander(
531
+ f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
532
+ expanded=False
533
+ ):
534
+ if 'messages' in chat and chat['messages']:
535
+ # Mostrar cada mensaje en la conversación
536
+ for message in chat['messages']:
537
+ role = message.get('role', 'unknown')
538
+ content = message.get('content', '')
539
+
540
+ # Usar el componente de chat de Streamlit
541
+ with st.chat_message(role):
542
+ st.markdown(content)
543
+
544
+ # Agregar separador entre mensajes
545
+ st.divider()
546
+ else:
547
+ st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
548
+
549
+ except Exception as e:
550
+ logger.error(f"Error mostrando conversación: {str(e)}")
551
+ continue
552
+
553
+ except Exception as e:
554
+ logger.error(f"Error mostrando historial del chat: {str(e)}")
555
+ st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
556
+
557
+ #################################################################################
558
+ def display_discourse_comparison(analysis: dict, t: dict):
559
+ """Muestra la comparación de análisis del discurso"""
560
+ st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
561
+
562
+ col1, col2 = st.columns(2)
563
+ with col1:
564
+ st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
565
+ df1 = pd.DataFrame(analysis['key_concepts1'])
566
+ st.dataframe(df1)
567
+
568
+ with col2:
569
+ st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
570
+ df2 = pd.DataFrame(analysis['key_concepts2'])
571
+ st.dataframe(df2)
modules/studentact/temp_current_situation_interface.py CHANGED
@@ -1,311 +1,311 @@
1
- # modules/studentact/current_situation_interface.py
2
-
3
- import streamlit as st
4
- import logging
5
- from ..utils.widget_utils import generate_unique_key
6
- from .current_situation_analysis import (
7
- analyze_text_dimensions,
8
- create_vocabulary_network,
9
- create_syntax_complexity_graph,
10
- create_cohesion_heatmap
11
- )
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
- def display_current_situation_interface(lang_code, nlp_models, t):
16
- """
17
- Interfaz modular para el análisis de la situación actual del estudiante.
18
- Esta función maneja la presentación y la interacción con el usuario.
19
-
20
- Args:
21
- lang_code: Código del idioma actual
22
- nlp_models: Diccionario de modelos de spaCy cargados
23
- t: Diccionario de traducciones
24
- """
25
- st.markdown("## Mi Situación Actual de Escritura")
26
-
27
- # Container principal para mejor organización visual
28
- with st.container():
29
- # Columnas para entrada y visualización
30
- text_col, visual_col = st.columns([1,2])
31
-
32
- with text_col:
33
- # Área de entrada de texto
34
- text_input = st.text_area(
35
- t.get('current_situation_input', "Ingresa tu texto para analizar:"),
36
- height=400,
37
- key=generate_unique_key("current_situation", "input")
38
- )
39
-
40
- # Botón de análisis
41
- if st.button(
42
- t.get('analyze_button', "Explorar mi escritura"),
43
- type="primary",
44
- disabled=not text_input,
45
- key=generate_unique_key("current_situation", "analyze")
46
- ):
47
- try:
48
- with st.spinner(t.get('processing', "Analizando texto...")):
49
- # 1. Procesar el texto
50
- doc = nlp_models[lang_code](text_input)
51
- metrics = analyze_text_dimensions(doc)
52
-
53
- # 2. Mostrar visualizaciones en la columna derecha
54
- with visual_col:
55
- display_current_situation_visual(doc, metrics)
56
-
57
- # 3. Obtener retroalimentación de Claude
58
- feedback = get_claude_feedback(metrics, text_input)
59
-
60
- # 4. Guardar los resultados
61
- from ..database.current_situation_mongo_db import store_current_situation_result
62
-
63
- if st.button(t.get('analyze_button', "Explorar mi escritura")):
64
- with st.spinner(t.get('processing', "Analizando texto...")):
65
- # Procesar y analizar
66
- doc = nlp_models[lang_code](text_input)
67
-
68
- # Obtener métricas con manejo de errores
69
- try:
70
- metrics = analyze_text_dimensions(doc)
71
- except Exception as e:
72
- logger.error(f"Error en análisis: {str(e)}")
73
- st.error("Error en el análisis de dimensiones")
74
- return
75
-
76
- # Obtener feedback
77
- try:
78
- feedback = get_claude_feedback(metrics, text_input)
79
- except Exception as e:
80
- logger.error(f"Error obteniendo feedback: {str(e)}")
81
- st.error("Error obteniendo retroalimentación")
82
- return
83
-
84
- # Guardar resultados con verificación
85
- if store_current_situation_result(
86
- st.session_state.username,
87
- text_input,
88
- metrics,
89
- feedback
90
- ):
91
- st.success(t.get('save_success', "Análisis guardado"))
92
-
93
- # Mostrar visualizaciones y recomendaciones
94
- display_current_situation_visual(doc, metrics)
95
- show_recommendations(feedback, t)
96
- else:
97
- st.error("Error al guardar el análisis")
98
-
99
- except Exception as e:
100
- logger.error(f"Error en interfaz: {str(e)}")
101
- st.error("Error general en la interfaz")
102
-
103
- ################################################################
104
- def display_current_situation_visual(doc, metrics):
105
- """Visualización mejorada de resultados con interpretaciones"""
106
- try:
107
- with st.container():
108
- # Estilos CSS mejorados para los contenedores
109
- st.markdown("""
110
- <style>
111
- .graph-container {
112
- background-color: white;
113
- border-radius: 10px;
114
- padding: 20px;
115
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
116
- margin: 15px 0;
117
- }
118
- .interpretation-box {
119
- background-color: #f8f9fa;
120
- border-left: 4px solid #0d6efd;
121
- padding: 15px;
122
- margin: 10px 0;
123
- }
124
- .metric-indicator {
125
- font-size: 1.2em;
126
- font-weight: 500;
127
- color: #1f2937;
128
- }
129
- </style>
130
- """, unsafe_allow_html=True)
131
-
132
- # 1. Riqueza de Vocabulario
133
- with st.expander("📚 Riqueza de Vocabulario", expanded=True):
134
- st.markdown('<div class="graph-container">', unsafe_allow_html=True)
135
- vocabulary_graph = create_vocabulary_network(doc)
136
- if vocabulary_graph:
137
- # Mostrar gráfico
138
- st.pyplot(vocabulary_graph)
139
- plt.close(vocabulary_graph)
140
-
141
- # Interpretación
142
- st.markdown('<div class="interpretation-box">', unsafe_allow_html=True)
143
- st.markdown("**¿Qué significa este gráfico?**")
144
- st.markdown("""
145
- - 🔵 Los nodos azules representan palabras clave en tu texto
146
- - 📏 El tamaño de cada nodo indica su frecuencia de uso
147
- - 🔗 Las líneas conectan palabras que aparecen juntas frecuentemente
148
- - 🎨 Los colores más intensos indican palabras más centrales
149
- """)
150
- st.markdown("</div>", unsafe_allow_html=True)
151
- st.markdown("</div>", unsafe_allow_html=True)
152
-
153
- # 2. Estructura de Oraciones
154
- with st.expander("🏗️ Complejidad Estructural", expanded=True):
155
- st.markdown('<div class="graph-container">', unsafe_allow_html=True)
156
- syntax_graph = create_syntax_complexity_graph(doc)
157
- if syntax_graph:
158
- st.pyplot(syntax_graph)
159
- plt.close(syntax_graph)
160
-
161
- st.markdown('<div class="interpretation-box">', unsafe_allow_html=True)
162
- st.markdown("**Análisis de la estructura:**")
163
- st.markdown("""
164
- - 📊 Las barras muestran la complejidad de cada oración
165
- - 📈 Mayor altura indica estructuras más elaboradas
166
- - 🎯 La línea punteada indica el nivel óptimo de complejidad
167
- - 🔄 Variación en las alturas sugiere dinamismo en la escritura
168
- """)
169
- st.markdown("</div>", unsafe_allow_html=True)
170
- st.markdown("</div>", unsafe_allow_html=True)
171
-
172
- # 3. Cohesión Textual
173
- with st.expander("🔄 Cohesión del Texto", expanded=True):
174
- st.markdown('<div class="graph-container">', unsafe_allow_html=True)
175
- cohesion_map = create_cohesion_heatmap(doc)
176
- if cohesion_map:
177
- st.pyplot(cohesion_map)
178
- plt.close(cohesion_map)
179
-
180
- st.markdown('<div class="interpretation-box">', unsafe_allow_html=True)
181
- st.markdown("**¿Cómo leer el mapa de calor?**")
182
- st.markdown("""
183
- - 🌈 Colores más intensos indican mayor conexión entre oraciones
184
- - 📝 La diagonal muestra la coherencia interna de cada oración
185
- - 🔗 Las zonas claras sugieren oportunidades de mejorar conexiones
186
- - 🎯 Un buen texto muestra patrones de color consistentes
187
- """)
188
- st.markdown("</div>", unsafe_allow_html=True)
189
- st.markdown("</div>", unsafe_allow_html=True)
190
-
191
- # 4. Métricas Generales
192
- with st.expander("📊 Resumen de Métricas", expanded=True):
193
- col1, col2, col3 = st.columns(3)
194
-
195
- with col1:
196
- st.metric(
197
- "Diversidad Léxica",
198
- f"{metrics['vocabulary_richness']:.2f}/1.0",
199
- help="Mide la variedad de palabras diferentes utilizadas"
200
- )
201
-
202
- with col2:
203
- st.metric(
204
- "Complejidad Estructural",
205
- f"{metrics['structural_complexity']:.2f}/1.0",
206
- help="Indica qué tan elaboradas son las estructuras de las oraciones"
207
- )
208
-
209
- with col3:
210
- st.metric(
211
- "Cohesión Textual",
212
- f"{metrics['cohesion_score']:.2f}/1.0",
213
- help="Evalúa qué tan bien conectadas están las ideas entre sí"
214
- )
215
-
216
- except Exception as e:
217
- logger.error(f"Error en visualización: {str(e)}")
218
- st.error("Error al generar las visualizaciones")
219
-
220
- ################################################################
221
- def show_recommendations(feedback, t):
222
- """
223
- Muestra las recomendaciones y ejercicios personalizados para el estudiante,
224
- permitiendo el seguimiento de su progreso.
225
-
226
- Args:
227
- feedback: Diccionario con retroalimentación y ejercicios recomendados
228
- t: Diccionario de traducciones
229
- """
230
- st.markdown("### " + t.get('recommendations_title', "Recomendaciones para mejorar"))
231
-
232
- for area, exercises in feedback['recommendations'].items():
233
- with st.expander(f"💡 {area}"):
234
- try:
235
- # Descripción del área de mejora
236
- st.markdown(exercises['description'])
237
-
238
- # Obtener el historial de ejercicios del estudiante
239
- from ..database.current_situation_mongo_db import get_student_exercises_history
240
- exercises_history = get_student_exercises_history(st.session_state.username)
241
-
242
- # Separar ejercicios en completados y pendientes
243
- completed = exercises_history.get(area, [])
244
-
245
- # Mostrar estado actual
246
- progress_col1, progress_col2 = st.columns([3,1])
247
- with progress_col1:
248
- st.markdown("**Ejercicio sugerido:**")
249
- st.markdown(exercises['activity'])
250
-
251
- with progress_col2:
252
- # Verificar si el ejercicio ya está completado
253
- exercise_key = f"{area}_{exercises['activity']}"
254
- is_completed = exercise_key in completed
255
-
256
- if is_completed:
257
- st.success("✅ Completado")
258
- else:
259
- # Botón para marcar ejercicio como completado
260
- if st.button(
261
- t.get('mark_complete', "Marcar como completado"),
262
- key=generate_unique_key("exercise", area),
263
- type="primary"
264
- ):
265
- try:
266
- from ..database.current_situation_mongo_db import update_exercise_status
267
-
268
- # Actualizar estado del ejercicio
269
- success = update_exercise_status(
270
- username=st.session_state.username,
271
- area=area,
272
- exercise=exercises['activity'],
273
- completed=True
274
- )
275
-
276
- if success:
277
- st.success(t.get(
278
- 'exercise_completed',
279
- "¡Ejercicio marcado como completado!"
280
- ))
281
- st.rerun()
282
- else:
283
- st.error(t.get(
284
- 'exercise_error',
285
- "Error al actualizar el estado del ejercicio"
286
- ))
287
- except Exception as e:
288
- logger.error(f"Error actualizando estado del ejercicio: {str(e)}")
289
- st.error(t.get('update_error', "Error al actualizar el ejercicio"))
290
-
291
- # Mostrar recursos adicionales si existen
292
- if 'resources' in exercises:
293
- st.markdown("**Recursos adicionales:**")
294
- for resource in exercises['resources']:
295
- st.markdown(f"- {resource}")
296
-
297
- # Mostrar fecha de finalización si está completado
298
- if is_completed:
299
- completion_date = exercises_history[exercise_key].get('completion_date')
300
- if completion_date:
301
- st.caption(
302
- t.get('completed_on', "Completado el") +
303
- f": {completion_date.strftime('%d/%m/%Y %H:%M')}"
304
- )
305
-
306
- except Exception as e:
307
- logger.error(f"Error mostrando recomendaciones para {area}: {str(e)}")
308
- st.error(t.get(
309
- 'recommendations_error',
310
- f"Error al mostrar las recomendaciones para {area}"
311
  ))
 
1
+ # modules/studentact/current_situation_interface.py
2
+
3
+ import streamlit as st
4
+ import logging
5
+ from ..utils.widget_utils import generate_unique_key
6
+ from .current_situation_analysis import (
7
+ analyze_text_dimensions,
8
+ create_vocabulary_network,
9
+ create_syntax_complexity_graph,
10
+ create_cohesion_heatmap
11
+ )
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ def display_current_situation_interface(lang_code, nlp_models, t):
16
+ """
17
+ Interfaz modular para el análisis de la situación actual del estudiante.
18
+ Esta función maneja la presentación y la interacción con el usuario.
19
+
20
+ Args:
21
+ lang_code: Código del idioma actual
22
+ nlp_models: Diccionario de modelos de spaCy cargados
23
+ t: Diccionario de traducciones
24
+ """
25
+ st.markdown("## Mi Situación Actual de Escritura")
26
+
27
+ # Container principal para mejor organización visual
28
+ with st.container():
29
+ # Columnas para entrada y visualización
30
+ text_col, visual_col = st.columns([1,2])
31
+
32
+ with text_col:
33
+ # Área de entrada de texto
34
+ text_input = st.text_area(
35
+ t.get('current_situation_input', "Ingresa tu texto para analizar:"),
36
+ height=400,
37
+ key=generate_unique_key("current_situation", "input")
38
+ )
39
+
40
+ # Botón de análisis
41
+ if st.button(
42
+ t.get('analyze_button', "Explorar mi escritura"),
43
+ type="primary",
44
+ disabled=not text_input,
45
+ key=generate_unique_key("current_situation", "analyze")
46
+ ):
47
+ try:
48
+ with st.spinner(t.get('processing', "Analizando texto...")):
49
+ # 1. Procesar el texto
50
+ doc = nlp_models[lang_code](text_input)
51
+ metrics = analyze_text_dimensions(doc)
52
+
53
+ # 2. Mostrar visualizaciones en la columna derecha
54
+ with visual_col:
55
+ display_current_situation_visual(doc, metrics)
56
+
57
+ # 3. Obtener retroalimentación de Claude
58
+ feedback = get_claude_feedback(metrics, text_input)
59
+
60
+ # 4. Guardar los resultados
61
+ from ..database.current_situation_mongo_db import store_current_situation_result
62
+
63
+ if st.button(t.get('analyze_button', "Explorar mi escritura")):
64
+ with st.spinner(t.get('processing', "Analizando texto...")):
65
+ # Procesar y analizar
66
+ doc = nlp_models[lang_code](text_input)
67
+
68
+ # Obtener métricas con manejo de errores
69
+ try:
70
+ metrics = analyze_text_dimensions(doc)
71
+ except Exception as e:
72
+ logger.error(f"Error en análisis: {str(e)}")
73
+ st.error("Error en el análisis de dimensiones")
74
+ return
75
+
76
+ # Obtener feedback
77
+ try:
78
+ feedback = get_claude_feedback(metrics, text_input)
79
+ except Exception as e:
80
+ logger.error(f"Error obteniendo feedback: {str(e)}")
81
+ st.error("Error obteniendo retroalimentación")
82
+ return
83
+
84
+ # Guardar resultados con verificación
85
+ if store_current_situation_result(
86
+ st.session_state.username,
87
+ text_input,
88
+ metrics,
89
+ feedback
90
+ ):
91
+ st.success(t.get('save_success', "Análisis guardado"))
92
+
93
+ # Mostrar visualizaciones y recomendaciones
94
+ display_current_situation_visual(doc, metrics)
95
+ show_recommendations(feedback, t)
96
+ else:
97
+ st.error("Error al guardar el análisis")
98
+
99
+ except Exception as e:
100
+ logger.error(f"Error en interfaz: {str(e)}")
101
+ st.error("Error general en la interfaz")
102
+
103
+ ################################################################
104
+ def display_current_situation_visual(doc, metrics):
105
+ """Visualización mejorada de resultados con interpretaciones"""
106
+ try:
107
+ with st.container():
108
+ # Estilos CSS mejorados para los contenedores
109
+ st.markdown("""
110
+ <style>
111
+ .graph-container {
112
+ background-color: white;
113
+ border-radius: 10px;
114
+ padding: 20px;
115
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
116
+ margin: 15px 0;
117
+ }
118
+ .interpretation-box {
119
+ background-color: #f8f9fa;
120
+ border-left: 4px solid #0d6efd;
121
+ padding: 15px;
122
+ margin: 10px 0;
123
+ }
124
+ .metric-indicator {
125
+ font-size: 1.2em;
126
+ font-weight: 500;
127
+ color: #1f2937;
128
+ }
129
+ </style>
130
+ """, unsafe_allow_html=True)
131
+
132
+ # 1. Riqueza de Vocabulario
133
+ with st.expander("📚 Riqueza de Vocabulario", expanded=True):
134
+ st.markdown('<div class="graph-container">', unsafe_allow_html=True)
135
+ vocabulary_graph = create_vocabulary_network(doc)
136
+ if vocabulary_graph:
137
+ # Mostrar gráfico
138
+ st.pyplot(vocabulary_graph)
139
+ plt.close(vocabulary_graph)
140
+
141
+ # Interpretación
142
+ st.markdown('<div class="interpretation-box">', unsafe_allow_html=True)
143
+ st.markdown("**¿Qué significa este gráfico?**")
144
+ st.markdown("""
145
+ - 🔵 Los nodos azules representan palabras clave en tu texto
146
+ - 📏 El tamaño de cada nodo indica su frecuencia de uso
147
+ - 🔗 Las líneas conectan palabras que aparecen juntas frecuentemente
148
+ - 🎨 Los colores más intensos indican palabras más centrales
149
+ """)
150
+ st.markdown("</div>", unsafe_allow_html=True)
151
+ st.markdown("</div>", unsafe_allow_html=True)
152
+
153
+ # 2. Estructura de Oraciones
154
+ with st.expander("🏗️ Complejidad Estructural", expanded=True):
155
+ st.markdown('<div class="graph-container">', unsafe_allow_html=True)
156
+ syntax_graph = create_syntax_complexity_graph(doc)
157
+ if syntax_graph:
158
+ st.pyplot(syntax_graph)
159
+ plt.close(syntax_graph)
160
+
161
+ st.markdown('<div class="interpretation-box">', unsafe_allow_html=True)
162
+ st.markdown("**Análisis de la estructura:**")
163
+ st.markdown("""
164
+ - 📊 Las barras muestran la complejidad de cada oración
165
+ - 📈 Mayor altura indica estructuras más elaboradas
166
+ - 🎯 La línea punteada indica el nivel óptimo de complejidad
167
+ - 🔄 Variación en las alturas sugiere dinamismo en la escritura
168
+ """)
169
+ st.markdown("</div>", unsafe_allow_html=True)
170
+ st.markdown("</div>", unsafe_allow_html=True)
171
+
172
+ # 3. Cohesión Textual
173
+ with st.expander("🔄 Cohesión del Texto", expanded=True):
174
+ st.markdown('<div class="graph-container">', unsafe_allow_html=True)
175
+ cohesion_map = create_cohesion_heatmap(doc)
176
+ if cohesion_map:
177
+ st.pyplot(cohesion_map)
178
+ plt.close(cohesion_map)
179
+
180
+ st.markdown('<div class="interpretation-box">', unsafe_allow_html=True)
181
+ st.markdown("**¿Cómo leer el mapa de calor?**")
182
+ st.markdown("""
183
+ - 🌈 Colores más intensos indican mayor conexión entre oraciones
184
+ - 📝 La diagonal muestra la coherencia interna de cada oración
185
+ - 🔗 Las zonas claras sugieren oportunidades de mejorar conexiones
186
+ - 🎯 Un buen texto muestra patrones de color consistentes
187
+ """)
188
+ st.markdown("</div>", unsafe_allow_html=True)
189
+ st.markdown("</div>", unsafe_allow_html=True)
190
+
191
+ # 4. Métricas Generales
192
+ with st.expander("📊 Resumen de Métricas", expanded=True):
193
+ col1, col2, col3 = st.columns(3)
194
+
195
+ with col1:
196
+ st.metric(
197
+ "Diversidad Léxica",
198
+ f"{metrics['vocabulary_richness']:.2f}/1.0",
199
+ help="Mide la variedad de palabras diferentes utilizadas"
200
+ )
201
+
202
+ with col2:
203
+ st.metric(
204
+ "Complejidad Estructural",
205
+ f"{metrics['structural_complexity']:.2f}/1.0",
206
+ help="Indica qué tan elaboradas son las estructuras de las oraciones"
207
+ )
208
+
209
+ with col3:
210
+ st.metric(
211
+ "Cohesión Textual",
212
+ f"{metrics['cohesion_score']:.2f}/1.0",
213
+ help="Evalúa qué tan bien conectadas están las ideas entre sí"
214
+ )
215
+
216
+ except Exception as e:
217
+ logger.error(f"Error en visualización: {str(e)}")
218
+ st.error("Error al generar las visualizaciones")
219
+
220
+ ################################################################
221
+ def show_recommendations(feedback, t):
222
+ """
223
+ Muestra las recomendaciones y ejercicios personalizados para el estudiante,
224
+ permitiendo el seguimiento de su progreso.
225
+
226
+ Args:
227
+ feedback: Diccionario con retroalimentación y ejercicios recomendados
228
+ t: Diccionario de traducciones
229
+ """
230
+ st.markdown("### " + t.get('recommendations_title', "Recomendaciones para mejorar"))
231
+
232
+ for area, exercises in feedback['recommendations'].items():
233
+ with st.expander(f"💡 {area}"):
234
+ try:
235
+ # Descripción del área de mejora
236
+ st.markdown(exercises['description'])
237
+
238
+ # Obtener el historial de ejercicios del estudiante
239
+ from ..database.current_situation_mongo_db import get_student_exercises_history
240
+ exercises_history = get_student_exercises_history(st.session_state.username)
241
+
242
+ # Separar ejercicios en completados y pendientes
243
+ completed = exercises_history.get(area, [])
244
+
245
+ # Mostrar estado actual
246
+ progress_col1, progress_col2 = st.columns([3,1])
247
+ with progress_col1:
248
+ st.markdown("**Ejercicio sugerido:**")
249
+ st.markdown(exercises['activity'])
250
+
251
+ with progress_col2:
252
+ # Verificar si el ejercicio ya está completado
253
+ exercise_key = f"{area}_{exercises['activity']}"
254
+ is_completed = exercise_key in completed
255
+
256
+ if is_completed:
257
+ st.success("✅ Completado")
258
+ else:
259
+ # Botón para marcar ejercicio como completado
260
+ if st.button(
261
+ t.get('mark_complete', "Marcar como completado"),
262
+ key=generate_unique_key("exercise", area),
263
+ type="primary"
264
+ ):
265
+ try:
266
+ from ..database.current_situation_mongo_db import update_exercise_status
267
+
268
+ # Actualizar estado del ejercicio
269
+ success = update_exercise_status(
270
+ username=st.session_state.username,
271
+ area=area,
272
+ exercise=exercises['activity'],
273
+ completed=True
274
+ )
275
+
276
+ if success:
277
+ st.success(t.get(
278
+ 'exercise_completed',
279
+ "¡Ejercicio marcado como completado!"
280
+ ))
281
+ st.rerun()
282
+ else:
283
+ st.error(t.get(
284
+ 'exercise_error',
285
+ "Error al actualizar el estado del ejercicio"
286
+ ))
287
+ except Exception as e:
288
+ logger.error(f"Error actualizando estado del ejercicio: {str(e)}")
289
+ st.error(t.get('update_error', "Error al actualizar el ejercicio"))
290
+
291
+ # Mostrar recursos adicionales si existen
292
+ if 'resources' in exercises:
293
+ st.markdown("**Recursos adicionales:**")
294
+ for resource in exercises['resources']:
295
+ st.markdown(f"- {resource}")
296
+
297
+ # Mostrar fecha de finalización si está completado
298
+ if is_completed:
299
+ completion_date = exercises_history[exercise_key].get('completion_date')
300
+ if completion_date:
301
+ st.caption(
302
+ t.get('completed_on', "Completado el") +
303
+ f": {completion_date.strftime('%d/%m/%Y %H:%M')}"
304
+ )
305
+
306
+ except Exception as e:
307
+ logger.error(f"Error mostrando recomendaciones para {area}: {str(e)}")
308
+ st.error(t.get(
309
+ 'recommendations_error',
310
+ f"Error al mostrar las recomendaciones para {area}"
311
  ))