AIdeaText commited on
Commit
802c793
·
verified ·
1 Parent(s): ddeda55

Update modules/studentact/current_situation_interface.py

Browse files
modules/studentact/current_situation_interface.py CHANGED
@@ -1,321 +1,353 @@
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")
 
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
+ # Obtener el mensaje de advertencia traducido si está disponible
20
+ warning = t.get('module_not_available', "Módulo de recomendaciones personalizadas no disponible. Por favor, contacte al administrador.")
21
+ st.warning(warning)
22
+
23
+ from .current_situation_analysis import (
24
+ analyze_text_dimensions,
25
+ analyze_clarity,
26
+ analyze_vocabulary_diversity,
27
+ analyze_cohesion,
28
+ analyze_structure,
29
+ get_dependency_depths,
30
+ normalize_score,
31
+ generate_sentence_graphs,
32
+ generate_word_connections,
33
+ generate_connection_paths,
34
+ create_vocabulary_network,
35
+ create_syntax_complexity_graph,
36
+ create_cohesion_heatmap
37
+ )
38
+
39
+ # Configuración del estilo de matplotlib para el gráfico de radar
40
+ plt.rcParams['font.family'] = 'sans-serif'
41
+ plt.rcParams['axes.grid'] = True
42
+ plt.rcParams['axes.spines.top'] = False
43
+ plt.rcParams['axes.spines.right'] = False
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ # Definición de tipos de texto con umbrales
48
+ TEXT_TYPES = {
49
+ 'academic_article': {
50
+ # Los nombres se obtendrán de las traducciones
51
+ 'thresholds': {
52
+ 'vocabulary': {'min': 0.70, 'target': 0.85},
53
+ 'structure': {'min': 0.75, 'target': 0.90},
54
+ 'cohesion': {'min': 0.65, 'target': 0.80},
55
+ 'clarity': {'min': 0.70, 'target': 0.85}
56
+ }
57
+ },
58
+ 'student_essay': {
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
+ 'thresholds': {
68
+ 'vocabulary': {'min': 0.50, 'target': 0.65},
69
+ 'structure': {'min': 0.55, 'target': 0.70},
70
+ 'cohesion': {'min': 0.45, 'target': 0.60},
71
+ 'clarity': {'min': 0.50, 'target': 0.65}
72
+ }
73
+ }
74
+ }
75
+
76
+ def display_current_situation_interface(lang_code, nlp_models, t):
77
+ """
78
+ Interfaz simplificada con gráfico de radar para visualizar métricas.
79
+ """
80
+ # Obtener traducciones de la situación actual
81
+ current_situation_t = t.get('CURRENT_SITUATION', {})
82
+
83
+ # Obtener traducciones para tipos de texto
84
+ text_types_translations = {}
85
+ if 'RECOMMENDATIONS' in t and lang_code in t['RECOMMENDATIONS']:
86
+ text_types_translations = t['RECOMMENDATIONS'][lang_code]['text_types']
87
+
88
+ # Inicializar estados si no existen
89
+ if 'text_input' not in st.session_state:
90
+ st.session_state.text_input = ""
91
+ if 'text_area' not in st.session_state:
92
+ st.session_state.text_area = ""
93
+ if 'show_results' not in st.session_state:
94
+ st.session_state.show_results = False
95
+ if 'current_doc' not in st.session_state:
96
+ st.session_state.current_doc = None
97
+ if 'current_metrics' not in st.session_state:
98
+ st.session_state.current_metrics = None
99
+ if 'current_recommendations' not in st.session_state:
100
+ st.session_state.current_recommendations = None
101
+
102
+ try:
103
+ # Container principal con dos columnas
104
+ with st.container():
105
+ input_col, results_col = st.columns([1,2])
106
+
107
+ with input_col:
108
+ # Text area con manejo de estado
109
+ text_input = st.text_area(
110
+ current_situation_t.get('input_prompt', "Escribe o pega tu texto aquí:"),
111
+ height=400,
112
+ key="text_area",
113
+ value=st.session_state.text_input,
114
+ help=current_situation_t.get('help', "Este texto será analizado para darte recomendaciones personalizadas")
115
+ )
116
+
117
+ # Función para manejar cambios de texto
118
+ if text_input != st.session_state.text_input:
119
+ st.session_state.text_input = text_input
120
+ st.session_state.show_results = False
121
+
122
+ if st.button(
123
+ current_situation_t.get('analyze_button', "Analizar mi escritura"),
124
+ type="primary",
125
+ disabled=not text_input.strip(),
126
+ use_container_width=True,
127
+ ):
128
+ try:
129
+ with st.spinner(current_situation_t.get('processing', "Analizando...")):
130
+ doc = nlp_models[lang_code](text_input)
131
+ metrics = analyze_text_dimensions(doc)
132
+
133
+ storage_success = store_current_situation_result(
134
+ username=st.session_state.username,
135
+ text=text_input,
136
+ metrics=metrics,
137
+ feedback=None
138
+ )
139
+
140
+ if not storage_success:
141
+ logger.warning("No se pudo guardar el análisis en la base de datos")
142
+
143
+ st.session_state.current_doc = doc
144
+ st.session_state.current_metrics = metrics
145
+ st.session_state.show_results = True
146
+
147
+ except Exception as e:
148
+ logger.error(f"Error en análisis: {str(e)}")
149
+ st.error(current_situation_t.get('analysis_error', "Error al analizar el texto"))
150
+
151
+ # Mostrar resultados en la columna derecha
152
+ with results_col:
153
+ if st.session_state.show_results and st.session_state.current_metrics is not None:
154
+ # Primero los radio buttons para tipo de texto
155
+ st.markdown(f"### {current_situation_t.get('text_type_header', 'Tipo de texto')}")
156
+
157
+ # Preparar opciones de tipos de texto con nombres traducidos
158
+ text_type_options = {}
159
+ for text_type_key in TEXT_TYPES.keys():
160
+ if text_type_key in text_types_translations:
161
+ text_type_options[text_type_key] = text_types_translations[text_type_key]
162
+ else:
163
+ # Fallback a nombres genéricos si no hay traducción
164
+ default_names = {
165
+ 'academic_article': 'Academic Article' if lang_code == 'en' else 'Артикул академічний' if lang_code == 'uk' else 'Artículo Académico',
166
+ 'student_essay': 'Student Essay' if lang_code == 'en' else 'Студентське есе' if lang_code == 'uk' else 'Trabajo Universitario',
167
+ 'general_communication': 'General Communication' if lang_code == 'en' else 'Загальна комунікація' if lang_code == 'uk' else 'Comunicación General'
168
+ }
169
+ text_type_options[text_type_key] = default_names.get(text_type_key, text_type_key)
170
+
171
+ text_type = st.radio(
172
+ label=current_situation_t.get('text_type_header', "Tipo de texto"),
173
+ options=list(TEXT_TYPES.keys()),
174
+ format_func=lambda x: text_type_options.get(x, x),
175
+ horizontal=True,
176
+ key="text_type_radio",
177
+ label_visibility="collapsed",
178
+ help=current_situation_t.get('text_type_help', "Selecciona el tipo de texto para ajustar los criterios de evaluación")
179
+ )
180
+
181
+ st.session_state.current_text_type = text_type
182
+
183
+ # Crear subtabs con nombres traducidos
184
+ diagnosis_tab = "Diagnosis" if lang_code == 'en' else "Діагностика" if lang_code == 'uk' else "Diagnóstico"
185
+ recommendations_tab = "Recommendations" if lang_code == 'en' else "Рекомендації" if lang_code == 'uk' else "Recomendaciones"
186
+
187
+ subtab1, subtab2 = st.tabs([diagnosis_tab, recommendations_tab])
188
+
189
+ # Mostrar resultados en el primer subtab
190
+ with subtab1:
191
+ display_diagnosis(
192
+ metrics=st.session_state.current_metrics,
193
+ text_type=text_type,
194
+ lang_code=lang_code,
195
+ t=current_situation_t
196
+ )
197
+
198
+ # Mostrar recomendaciones en el segundo subtab
199
+ with subtab2:
200
+ # Llamar directamente a la función de recomendaciones personalizadas
201
+ display_personalized_recommendations(
202
+ text=text_input,
203
+ metrics=st.session_state.current_metrics,
204
+ text_type=text_type,
205
+ lang_code=lang_code,
206
+ t=t
207
+ )
208
+
209
+ except Exception as e:
210
+ logger.error(f"Error en interfaz principal: {str(e)}")
211
+ st.error(current_situation_t.get('error_interface', "Ocurrió un error al cargar la interfaz"))
212
+
213
+ def display_diagnosis(metrics, text_type=None, lang_code='es', t=None):
214
+ """
215
+ Muestra los resultados del análisis: métricas verticalmente y gráfico radar.
216
+ """
217
+ try:
218
+ # Asegurar que tenemos traducciones
219
+ if t is None:
220
+ t = {}
221
+
222
+ # Usar valor por defecto si no se especifica tipo
223
+ text_type = text_type or 'student_essay'
224
+
225
+ # Obtener umbrales según el tipo de texto
226
+ thresholds = TEXT_TYPES[text_type]['thresholds']
227
+
228
+ # Crear dos columnas para las métricas y el gráfico
229
+ metrics_col, graph_col = st.columns([1, 1.5])
230
+
231
+ # Columna de métricas
232
+ with metrics_col:
233
+ metrics_config = [
234
+ {
235
+ 'label': t.get('vocabulary_label', "Vocabulario"),
236
+ 'key': 'vocabulary',
237
+ 'value': metrics['vocabulary']['normalized_score'],
238
+ 'help': t.get('vocabulary_help', "Riqueza y variedad del vocabulario"),
239
+ 'thresholds': thresholds['vocabulary']
240
+ },
241
+ {
242
+ 'label': t.get('structure_label', "Estructura"),
243
+ 'key': 'structure',
244
+ 'value': metrics['structure']['normalized_score'],
245
+ 'help': t.get('structure_help', "Organización y complejidad de oraciones"),
246
+ 'thresholds': thresholds['structure']
247
+ },
248
+ {
249
+ 'label': t.get('cohesion_label', "Cohesión"),
250
+ 'key': 'cohesion',
251
+ 'value': metrics['cohesion']['normalized_score'],
252
+ 'help': t.get('cohesion_help', "Conexión y fluidez entre ideas"),
253
+ 'thresholds': thresholds['cohesion']
254
+ },
255
+ {
256
+ 'label': t.get('clarity_label', "Claridad"),
257
+ 'key': 'clarity',
258
+ 'value': metrics['clarity']['normalized_score'],
259
+ 'help': t.get('clarity_help', "Facilidad de comprensión del texto"),
260
+ 'thresholds': thresholds['clarity']
261
+ }
262
+ ]
263
+
264
+ # Mostrar métricas con textos traducidos
265
+ for metric in metrics_config:
266
+ value = metric['value']
267
+ if value < metric['thresholds']['min']:
268
+ status = t.get('metric_improvement', "⚠️ Por mejorar")
269
+ color = "inverse"
270
+ elif value < metric['thresholds']['target']:
271
+ status = t.get('metric_acceptable', "📈 Aceptable")
272
+ color = "off"
273
+ else:
274
+ status = t.get('metric_optimal', "✅ Óptimo")
275
+ color = "normal"
276
+
277
+ target_text = t.get('metric_target', "Meta: {:.2f}").format(metric['thresholds']['target'])
278
+
279
+ st.metric(
280
+ metric['label'],
281
+ f"{value:.2f}",
282
+ f"{status} ({target_text})",
283
+ delta_color=color,
284
+ help=metric['help']
285
+ )
286
+ st.markdown("<div style='margin-bottom: 0.5rem;'></div>", unsafe_allow_html=True)
287
+
288
+ # Gráfico radar en la columna derecha
289
+ with graph_col:
290
+ display_radar_chart(metrics_config, thresholds)
291
+
292
+ except Exception as e:
293
+ logger.error(f"Error mostrando resultados: {str(e)}")
294
+ st.error(t.get('error_results', "Error al mostrar los resultados"))
295
+
296
+ def display_radar_chart(metrics_config, thresholds):
297
+ """
298
+ Muestra el gráfico radar con los resultados.
299
+ """
300
+ try:
301
+ # Preparar datos para el gráfico
302
+ categories = [m['label'] for m in metrics_config]
303
+ values_user = [m['value'] for m in metrics_config]
304
+ min_values = [m['thresholds']['min'] for m in metrics_config]
305
+ target_values = [m['thresholds']['target'] for m in metrics_config]
306
+
307
+ # Crear y configurar gráfico
308
+ fig = plt.figure(figsize=(8, 8))
309
+ ax = fig.add_subplot(111, projection='polar')
310
+
311
+ # Configurar radar
312
+ angles = [n / float(len(categories)) * 2 * np.pi for n in range(len(categories))]
313
+ angles += angles[:1]
314
+ values_user += values_user[:1]
315
+ min_values += min_values[:1]
316
+ target_values += target_values[:1]
317
+
318
+ # Configurar ejes
319
+ ax.set_xticks(angles[:-1])
320
+ ax.set_xticklabels(categories, fontsize=10)
321
+ circle_ticks = np.arange(0, 1.1, 0.2)
322
+ ax.set_yticks(circle_ticks)
323
+ ax.set_yticklabels([f'{tick:.1f}' for tick in circle_ticks], fontsize=8)
324
+ ax.set_ylim(0, 1)
325
+
326
+ # Dibujar áreas de umbrales
327
+ ax.plot(angles, min_values, '#e74c3c', linestyle='--', linewidth=1, label='Mínimo', alpha=0.5)
328
+ ax.plot(angles, target_values, '#2ecc71', linestyle='--', linewidth=1, label='Meta', alpha=0.5)
329
+ ax.fill_between(angles, target_values, [1]*len(angles), color='#2ecc71', alpha=0.1)
330
+ ax.fill_between(angles, [0]*len(angles), min_values, color='#e74c3c', alpha=0.1)
331
+
332
+ # Dibujar valores del usuario
333
+ ax.plot(angles, values_user, '#3498db', linewidth=2, label='Tu escritura')
334
+ ax.fill(angles, values_user, '#3498db', alpha=0.2)
335
+
336
+ # Ajustar leyenda
337
+ ax.legend(
338
+ loc='upper right',
339
+ bbox_to_anchor=(1.3, 1.1),
340
+ fontsize=10,
341
+ frameon=True,
342
+ facecolor='white',
343
+ edgecolor='none',
344
+ shadow=True
345
+ )
346
+
347
+ plt.tight_layout()
348
+ st.pyplot(fig)
349
+ plt.close()
350
+
351
+ except Exception as e:
352
+ logger.error(f"Error mostrando gráfico radar: {str(e)}")
353
  st.error("Error al mostrar el gráfico")