AIdeaText commited on
Commit
31d13c9
·
verified ·
1 Parent(s): a5d8590

Update modules/studentact/student_activities_v2.py

Browse files
modules/studentact/student_activities_v2.py CHANGED
@@ -1,576 +1,576 @@
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
- # Cambiado de "Mis Actividades" a "Registro de mis actividades"
44
- #st.header(t.get('activities_title', 'Registro de mis actividades'))
45
-
46
- # Tabs para diferentes tipos de análisis
47
- # Cambiado "Análisis del Discurso" a "Análisis comparado de textos"
48
- tabs = st.tabs([
49
- t.get('current_situation_activities', 'Registros de la función: Mi Situación Actual'),
50
- t.get('morpho_activities', 'Registros de mis análisis morfosintácticos'),
51
- t.get('semantic_activities', 'Registros de mis análisis semánticos'),
52
- t.get('discourse_activities', 'Registros de mis análisis comparado de textos'),
53
- t.get('chat_activities', 'Registros de mis conversaciones con el tutor virtual')
54
- ])
55
-
56
- # Tab de Situación Actual
57
- with tabs[0]:
58
- display_current_situation_activities(username, t)
59
-
60
- # Tab de Análisis Morfosintáctico
61
- with tabs[1]:
62
- display_morphosyntax_activities(username, t)
63
-
64
- # Tab de Análisis Semántico
65
- with tabs[2]:
66
- display_semantic_activities(username, t)
67
-
68
- # Tab de Análisis del Discurso (mantiene nombre interno pero UI muestra "Análisis comparado de textos")
69
- with tabs[3]:
70
- display_discourse_activities(username, t)
71
-
72
- # Tab de Conversaciones del Chat
73
- with tabs[4]:
74
- display_chat_activities(username, t)
75
-
76
- except Exception as e:
77
- logger.error(f"Error mostrando actividades: {str(e)}")
78
- st.error(t.get('error_loading_activities', 'Error al cargar las actividades'))
79
-
80
-
81
- ###############################################################################################
82
-
83
- def display_current_situation_activities(username: str, t: dict):
84
- """
85
- Muestra análisis de situación actual junto con las recomendaciones de Claude
86
- unificando la información de ambas colecciones y emparejándolas por cercanía temporal.
87
- """
88
- try:
89
- # Recuperar datos de ambas colecciones
90
- logger.info(f"Recuperando análisis de situación actual para {username}")
91
- situation_analyses = get_current_situation_analysis(username, limit=10)
92
-
93
- # Verificar si hay datos
94
- if situation_analyses:
95
- logger.info(f"Recuperados {len(situation_analyses)} análisis de situación")
96
- # Depurar para ver la estructura de datos
97
- for i, analysis in enumerate(situation_analyses):
98
- logger.info(f"Análisis #{i+1}: Claves disponibles: {list(analysis.keys())}")
99
- if 'metrics' in analysis:
100
- logger.info(f"Métricas disponibles: {list(analysis['metrics'].keys())}")
101
- else:
102
- logger.warning("No se encontraron análisis de situación actual")
103
-
104
- logger.info(f"Recuperando recomendaciones de Claude para {username}")
105
- claude_recommendations = get_claude_recommendations(username)
106
-
107
- if claude_recommendations:
108
- logger.info(f"Recuperadas {len(claude_recommendations)} recomendaciones de Claude")
109
- else:
110
- logger.warning("No se encontraron recomendaciones de Claude")
111
-
112
- # Verificar si hay algún tipo de análisis disponible
113
- if not situation_analyses and not claude_recommendations:
114
- logger.info("No se encontraron análisis de situación actual ni recomendaciones")
115
- st.info(t.get('no_current_situation', 'No hay análisis de situación actual registrados'))
116
- return
117
-
118
- # Crear pares combinados emparejando diagnósticos y recomendaciones cercanos en tiempo
119
- logger.info("Creando emparejamientos temporales de análisis")
120
-
121
- # Convertir timestamps a objetos datetime para comparación
122
- situation_times = []
123
- for analysis in situation_analyses:
124
- if 'timestamp' in analysis:
125
- try:
126
- timestamp_str = analysis['timestamp']
127
- dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
128
- situation_times.append((dt, analysis))
129
- except Exception as e:
130
- logger.error(f"Error parseando timestamp de situación: {str(e)}")
131
-
132
- recommendation_times = []
133
- for recommendation in claude_recommendations:
134
- if 'timestamp' in recommendation:
135
- try:
136
- timestamp_str = recommendation['timestamp']
137
- dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
138
- recommendation_times.append((dt, recommendation))
139
- except Exception as e:
140
- logger.error(f"Error parseando timestamp de recomendación: {str(e)}")
141
-
142
- # Ordenar por tiempo
143
- situation_times.sort(key=lambda x: x[0], reverse=True)
144
- recommendation_times.sort(key=lambda x: x[0], reverse=True)
145
-
146
- # Crear pares combinados
147
- combined_items = []
148
-
149
- # Primero, procesar todas las situaciones encontrando la recomendación más cercana
150
- for sit_time, situation in situation_times:
151
- # Buscar la recomendación más cercana en tiempo
152
- best_match = None
153
- min_diff = timedelta(minutes=30) # Máxima diferencia de tiempo aceptable (30 minutos)
154
- best_rec_time = None
155
-
156
- for rec_time, recommendation in recommendation_times:
157
- time_diff = abs(sit_time - rec_time)
158
- if time_diff < min_diff:
159
- min_diff = time_diff
160
- best_match = recommendation
161
- best_rec_time = rec_time
162
-
163
- # Crear un elemento combinado
164
- if best_match:
165
- timestamp_key = sit_time.isoformat()
166
- combined_items.append((timestamp_key, {
167
- 'situation': situation,
168
- 'recommendation': best_match,
169
- 'time_diff': min_diff.total_seconds()
170
- }))
171
- # Eliminar la recomendación usada para no reutilizarla
172
- recommendation_times = [(t, r) for t, r in recommendation_times if t != best_rec_time]
173
- logger.info(f"Emparejado: Diagnóstico {sit_time} con Recomendación {best_rec_time} (diferencia: {min_diff})")
174
- else:
175
- # Si no hay recomendación cercana, solo incluir la situación
176
- timestamp_key = sit_time.isoformat()
177
- combined_items.append((timestamp_key, {
178
- 'situation': situation
179
- }))
180
- logger.info(f"Sin emparejar: Diagnóstico {sit_time} sin recomendación cercana")
181
-
182
- # Agregar recomendaciones restantes sin situación
183
- for rec_time, recommendation in recommendation_times:
184
- timestamp_key = rec_time.isoformat()
185
- combined_items.append((timestamp_key, {
186
- 'recommendation': recommendation
187
- }))
188
- logger.info(f"Sin emparejar: Recomendación {rec_time} sin diagnóstico cercano")
189
-
190
- # Ordenar por tiempo (más reciente primero)
191
- combined_items.sort(key=lambda x: x[0], reverse=True)
192
-
193
- logger.info(f"Procesando {len(combined_items)} elementos combinados")
194
-
195
- # Mostrar cada par combinado
196
- for i, (timestamp_key, analysis_pair) in enumerate(combined_items):
197
- try:
198
- # Obtener datos de situación y recomendación
199
- situation_data = analysis_pair.get('situation', {})
200
- recommendation_data = analysis_pair.get('recommendation', {})
201
- time_diff = analysis_pair.get('time_diff')
202
-
203
- # Si no hay ningún dato, continuar al siguiente
204
- if not situation_data and not recommendation_data:
205
- continue
206
-
207
- # Determinar qué texto mostrar (priorizar el de la situación)
208
- text_to_show = situation_data.get('text', recommendation_data.get('text', ''))
209
- text_type = situation_data.get('text_type', recommendation_data.get('text_type', ''))
210
-
211
- # Formatear fecha para mostrar
212
- try:
213
- # Usar timestamp del key que ya es un formato ISO
214
- dt = datetime.fromisoformat(timestamp_key)
215
- formatted_date = dt.strftime("%d/%m/%Y %H:%M:%S")
216
- except Exception as date_error:
217
- logger.error(f"Error formateando fecha: {str(date_error)}")
218
- formatted_date = timestamp_key
219
-
220
- # Determinar el título del expander
221
- title = f"{t.get('analysis_date', 'Fecha')}: {formatted_date}"
222
- if text_type:
223
- text_type_display = {
224
- 'academic_article': t.get('academic_article', 'Artículo académico'),
225
- 'student_essay': t.get('student_essay', 'Trabajo universitario'),
226
- 'general_communication': t.get('general_communication', 'Comunicación general')
227
- }.get(text_type, text_type)
228
- title += f" - {text_type_display}"
229
-
230
- # Añadir indicador de emparejamiento si existe
231
- if time_diff is not None:
232
- if time_diff < 60: # menos de un minuto
233
- title += f" 🔄 (emparejados)"
234
- else:
235
- title += f" 🔄 (emparejados, diferencia: {int(time_diff//60)} min)"
236
-
237
- # Usar un ID único para cada expander
238
- expander_id = f"analysis_{i}_{timestamp_key.replace(':', '_')}"
239
-
240
- # Mostrar el análisis en un expander
241
- with st.expander(title, expanded=False):
242
- # Mostrar texto analizado con key único
243
- st.subheader(t.get('analyzed_text', 'Texto analizado'))
244
- st.text_area(
245
- "Text Content",
246
- value=text_to_show,
247
- height=100,
248
- disabled=True,
249
- label_visibility="collapsed",
250
- key=f"text_area_{expander_id}"
251
- )
252
-
253
- # Crear tabs para separar diagnóstico y recomendaciones
254
- diagnosis_tab, recommendations_tab = st.tabs([
255
- t.get('diagnosis_tab', 'Diagnóstico'),
256
- t.get('recommendations_tab', 'Recomendaciones')
257
- ])
258
-
259
- # Tab de diagnóstico
260
- with diagnosis_tab:
261
- if situation_data and 'metrics' in situation_data:
262
- metrics = situation_data['metrics']
263
-
264
- # Dividir en dos columnas
265
- col1, col2 = st.columns(2)
266
-
267
- # Principales métricas en formato de tarjetas
268
- with col1:
269
- st.subheader(t.get('key_metrics', 'Métricas clave'))
270
-
271
- # Mostrar cada métrica principal
272
- for metric_name, metric_data in metrics.items():
273
- try:
274
- # Determinar la puntuación
275
- score = None
276
- if isinstance(metric_data, dict):
277
- # Intentar diferentes nombres de campo
278
- if 'normalized_score' in metric_data:
279
- score = metric_data['normalized_score']
280
- elif 'score' in metric_data:
281
- score = metric_data['score']
282
- elif 'value' in metric_data:
283
- score = metric_data['value']
284
- elif isinstance(metric_data, (int, float)):
285
- score = metric_data
286
-
287
- if score is not None:
288
- # Asegurarse de que score es numérico
289
- if isinstance(score, (int, float)):
290
- # Determinar color y emoji basado en la puntuación
291
- if score < 0.5:
292
- emoji = "🔴"
293
- color = "#ffcccc" # light red
294
- elif score < 0.75:
295
- emoji = "🟡"
296
- color = "#ffffcc" # light yellow
297
- else:
298
- emoji = "🟢"
299
- color = "#ccffcc" # light green
300
-
301
- # Mostrar la métrica con estilo
302
- st.markdown(f"""
303
- <div style="background-color:{color}; padding:10px; border-radius:5px; margin-bottom:10px;">
304
- <b>{emoji} {metric_name.capitalize()}:</b> {score:.2f}
305
- </div>
306
- """, unsafe_allow_html=True)
307
- else:
308
- # Si no es numérico, mostrar como texto
309
- st.markdown(f"""
310
- <div style="background-color:#f0f0f0; padding:10px; border-radius:5px; margin-bottom:10px;">
311
- <b>ℹ️ {metric_name.capitalize()}:</b> {str(score)}
312
- </div>
313
- """, unsafe_allow_html=True)
314
- except Exception as e:
315
- logger.error(f"Error procesando métrica {metric_name}: {str(e)}")
316
-
317
- # Mostrar detalles adicionales si están disponibles
318
- with col2:
319
- st.subheader(t.get('details', 'Detalles'))
320
-
321
- # Para cada métrica, mostrar sus detalles si existen
322
- for metric_name, metric_data in metrics.items():
323
- try:
324
- if isinstance(metric_data, dict):
325
- # Mostrar detalles directamente o buscar en subcampos
326
- details = None
327
- if 'details' in metric_data and metric_data['details']:
328
- details = metric_data['details']
329
- else:
330
- # Crear un diccionario con los detalles excluyendo 'normalized_score' y similares
331
- details = {k: v for k, v in metric_data.items()
332
- if k not in ['normalized_score', 'score', 'value']}
333
-
334
- if details:
335
- st.write(f"**{metric_name.capitalize()}**")
336
- st.json(details, expanded=False)
337
- except Exception as e:
338
- logger.error(f"Error mostrando detalles de {metric_name}: {str(e)}")
339
- else:
340
- st.info(t.get('no_diagnosis', 'No hay datos de diagnóstico disponibles'))
341
-
342
- # Tab de recomendaciones
343
- with recommendations_tab:
344
- if recommendation_data and 'recommendations' in recommendation_data:
345
- st.markdown(f"""
346
- <div style="padding: 20px; border-radius: 10px;
347
- background-color: #f8f9fa; margin-bottom: 20px;">
348
- {recommendation_data['recommendations']}
349
- </div>
350
- """, unsafe_allow_html=True)
351
- elif recommendation_data and 'feedback' in recommendation_data:
352
- st.markdown(f"""
353
- <div style="padding: 20px; border-radius: 10px;
354
- background-color: #f8f9fa; margin-bottom: 20px;">
355
- {recommendation_data['feedback']}
356
- </div>
357
- """, unsafe_allow_html=True)
358
- else:
359
- st.info(t.get('no_recommendations', 'No hay recomendaciones disponibles'))
360
-
361
- except Exception as e:
362
- logger.error(f"Error procesando par de análisis: {str(e)}")
363
- continue
364
-
365
- except Exception as e:
366
- logger.error(f"Error mostrando actividades de situación actual: {str(e)}")
367
- st.error(t.get('error_current_situation', 'Error al mostrar análisis de situación actual'))
368
-
369
- ###############################################################################################
370
-
371
- def display_morphosyntax_activities(username: str, t: dict):
372
- """Muestra actividades de análisis morfosintáctico"""
373
- try:
374
- analyses = get_student_morphosyntax_analysis(username)
375
- if not analyses:
376
- st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
377
- return
378
-
379
- for analysis in analyses:
380
- with st.expander(
381
- f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
382
- expanded=False
383
- ):
384
- st.text(f"{t.get('analyzed_text', 'Texto analizado')}:")
385
- st.write(analysis['text'])
386
-
387
- if 'arc_diagrams' in analysis:
388
- st.subheader(t.get('syntactic_diagrams', 'Diagramas sintácticos'))
389
- for diagram in analysis['arc_diagrams']:
390
- st.write(diagram, unsafe_allow_html=True)
391
-
392
- except Exception as e:
393
- logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
394
- st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
395
-
396
-
397
- ###############################################################################################
398
-
399
- def display_semantic_activities(username: str, t: dict):
400
- """Muestra actividades de análisis semántico"""
401
- try:
402
- logger.info(f"Recuperando análisis semántico para {username}")
403
- analyses = get_student_semantic_analysis(username)
404
-
405
- if not analyses:
406
- logger.info("No se encontraron análisis semánticos")
407
- st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
408
- return
409
-
410
- logger.info(f"Procesando {len(analyses)} análisis semánticos")
411
-
412
- for analysis in analyses:
413
- try:
414
- # Verificar campos necesarios
415
- if not all(key in analysis for key in ['timestamp', 'concept_graph']):
416
- logger.warning(f"Análisis incompleto: {analysis.keys()}")
417
- continue
418
-
419
- # Formatear fecha
420
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
421
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
422
-
423
- # Crear expander
424
- with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
425
- # Procesar y mostrar gráfico
426
- if analysis.get('concept_graph'):
427
- try:
428
- # Convertir de base64 a bytes
429
- logger.debug("Decodificando gráfico de conceptos")
430
- image_data = analysis['concept_graph']
431
-
432
- # Si el gráfico ya es bytes, usarlo directamente
433
- if isinstance(image_data, bytes):
434
- image_bytes = image_data
435
- else:
436
- # Si es string base64, decodificar
437
- image_bytes = base64.b64decode(image_data)
438
-
439
- logger.debug(f"Longitud de bytes de imagen: {len(image_bytes)}")
440
-
441
- # Mostrar imagen
442
- st.image(
443
- image_bytes,
444
- caption=t.get('concept_network', 'Red de Conceptos'),
445
- use_column_width=True
446
- )
447
- logger.debug("Gráfico mostrado exitosamente")
448
-
449
- except Exception as img_error:
450
- logger.error(f"Error procesando gráfico: {str(img_error)}")
451
- st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
452
- else:
453
- st.info(t.get('no_graph', 'No hay visualización disponible'))
454
-
455
- except Exception as e:
456
- logger.error(f"Error procesando análisis individual: {str(e)}")
457
- continue
458
-
459
- except Exception as e:
460
- logger.error(f"Error mostrando análisis semántico: {str(e)}")
461
- st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
462
-
463
-
464
- ###################################################################################################
465
- def display_discourse_activities(username: str, t: dict):
466
- """Muestra actividades de análisis del discurso (mostrado como 'Análisis comparado de textos' en la UI)"""
467
- try:
468
- logger.info(f"Recuperando análisis del discurso para {username}")
469
- analyses = get_student_discourse_analysis(username)
470
-
471
- if not analyses:
472
- logger.info("No se encontraron análisis del discurso")
473
- # Usamos el término "análisis comparado de textos" en la UI
474
- st.info(t.get('no_discourse_analyses', 'No hay análisis comparados de textos registrados'))
475
- return
476
-
477
- logger.info(f"Procesando {len(analyses)} análisis del discurso")
478
- for analysis in analyses:
479
- try:
480
- # Verificar campos mínimos necesarios
481
- if not all(key in analysis for key in ['timestamp', 'combined_graph']):
482
- logger.warning(f"Análisis incompleto: {analysis.keys()}")
483
- continue
484
-
485
- # Formatear fecha
486
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
487
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
488
-
489
- with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
490
- if analysis['combined_graph']:
491
- logger.debug("Decodificando gráfico combinado")
492
- try:
493
- image_bytes = base64.b64decode(analysis['combined_graph'])
494
- st.image(image_bytes, use_column_width=True)
495
- logger.debug("Gráfico mostrado exitosamente")
496
- except Exception as img_error:
497
- logger.error(f"Error decodificando imagen: {str(img_error)}")
498
- st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
499
- else:
500
- st.info(t.get('no_visualization', 'No hay visualización comparativa disponible'))
501
-
502
- except Exception as e:
503
- logger.error(f"Error procesando análisis individual: {str(e)}")
504
- continue
505
-
506
- except Exception as e:
507
- logger.error(f"Error mostrando análisis del discurso: {str(e)}")
508
- # Usamos el término "análisis comparado de textos" en la UI
509
- st.error(t.get('error_discourse', 'Error al mostrar análisis comparado de textos'))
510
-
511
- #################################################################################
512
- def display_chat_activities(username: str, t: dict):
513
- """
514
- Muestra historial de conversaciones del chat
515
- """
516
- try:
517
- # Obtener historial del chat
518
- chat_history = get_chat_history(
519
- username=username,
520
- analysis_type='sidebar',
521
- limit=50
522
- )
523
-
524
- if not chat_history:
525
- st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
526
- return
527
-
528
- for chat in reversed(chat_history): # Mostrar las más recientes primero
529
- try:
530
- # Convertir timestamp a datetime para formato
531
- timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
532
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
533
-
534
- with st.expander(
535
- f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
536
- expanded=False
537
- ):
538
- if 'messages' in chat and chat['messages']:
539
- # Mostrar cada mensaje en la conversación
540
- for message in chat['messages']:
541
- role = message.get('role', 'unknown')
542
- content = message.get('content', '')
543
-
544
- # Usar el componente de chat de Streamlit
545
- with st.chat_message(role):
546
- st.markdown(content)
547
-
548
- # Agregar separador entre mensajes
549
- st.divider()
550
- else:
551
- st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
552
-
553
- except Exception as e:
554
- logger.error(f"Error mostrando conversación: {str(e)}")
555
- continue
556
-
557
- except Exception as e:
558
- logger.error(f"Error mostrando historial del chat: {str(e)}")
559
- st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
560
-
561
- #################################################################################
562
- def display_discourse_comparison(analysis: dict, t: dict):
563
- """Muestra la comparación de análisis del discurso"""
564
- # Cambiado para usar "textos comparados" en la UI
565
- st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
566
-
567
- col1, col2 = st.columns(2)
568
- with col1:
569
- st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
570
- df1 = pd.DataFrame(analysis['key_concepts1'])
571
- st.dataframe(df1)
572
-
573
- with col2:
574
- st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
575
- df2 = pd.DataFrame(analysis['key_concepts2'])
576
  st.dataframe(df2)
 
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
+ # Cambiado de "Mis Actividades" a "Registro de mis actividades"
44
+ #st.header(t.get('activities_title', 'Registro de mis actividades'))
45
+
46
+ # Tabs para diferentes tipos de análisis
47
+ # Cambiado "Análisis del Discurso" a "Análisis comparado de textos"
48
+ tabs = st.tabs([
49
+ t.get('current_situation_activities', 'Registros de la función: Mi Situación Actual'),
50
+ t.get('morpho_activities', 'Registros de mis análisis morfosintácticos'),
51
+ t.get('semantic_activities', 'Registros de mis análisis semánticos'),
52
+ t.get('discourse_activities', 'Registros de mis análisis comparado de textos'),
53
+ t.get('chat_activities', 'Registros de mis conversaciones con el tutor virtual')
54
+ ])
55
+
56
+ # Tab de Situación Actual
57
+ with tabs[0]:
58
+ display_current_situation_activities(username, t)
59
+
60
+ # Tab de Análisis Morfosintáctico
61
+ with tabs[1]:
62
+ display_morphosyntax_activities(username, t)
63
+
64
+ # Tab de Análisis Semántico
65
+ with tabs[2]:
66
+ display_semantic_activities(username, t)
67
+
68
+ # Tab de Análisis del Discurso (mantiene nombre interno pero UI muestra "Análisis comparado de textos")
69
+ with tabs[3]:
70
+ display_discourse_activities(username, t)
71
+
72
+ # Tab de Conversaciones del Chat
73
+ with tabs[4]:
74
+ display_chat_activities(username, t)
75
+
76
+ except Exception as e:
77
+ logger.error(f"Error mostrando actividades: {str(e)}")
78
+ st.error(t.get('error_loading_activities', 'Error al cargar las actividades'))
79
+
80
+
81
+ ###############################################################################################
82
+
83
+ def display_current_situation_activities(username: str, t: dict):
84
+ """
85
+ Muestra análisis de situación actual junto con las recomendaciones de Claude
86
+ unificando la información de ambas colecciones y emparejándolas por cercanía temporal.
87
+ """
88
+ try:
89
+ # Recuperar datos de ambas colecciones
90
+ logger.info(f"Recuperando análisis de situación actual para {username}")
91
+ situation_analyses = get_current_situation_analysis(username, limit=10)
92
+
93
+ # Verificar si hay datos
94
+ if situation_analyses:
95
+ logger.info(f"Recuperados {len(situation_analyses)} análisis de situación")
96
+ # Depurar para ver la estructura de datos
97
+ for i, analysis in enumerate(situation_analyses):
98
+ logger.info(f"Análisis #{i+1}: Claves disponibles: {list(analysis.keys())}")
99
+ if 'metrics' in analysis:
100
+ logger.info(f"Métricas disponibles: {list(analysis['metrics'].keys())}")
101
+ else:
102
+ logger.warning("No se encontraron análisis de situación actual")
103
+
104
+ logger.info(f"Recuperando recomendaciones de Claude para {username}")
105
+ claude_recommendations = get_claude_recommendations(username)
106
+
107
+ if claude_recommendations:
108
+ logger.info(f"Recuperadas {len(claude_recommendations)} recomendaciones de Claude")
109
+ else:
110
+ logger.warning("No se encontraron recomendaciones de Claude")
111
+
112
+ # Verificar si hay algún tipo de análisis disponible
113
+ if not situation_analyses and not claude_recommendations:
114
+ logger.info("No se encontraron análisis de situación actual ni recomendaciones")
115
+ st.info(t.get('no_current_situation', 'No hay análisis de situación actual registrados'))
116
+ return
117
+
118
+ # Crear pares combinados emparejando diagnósticos y recomendaciones cercanos en tiempo
119
+ logger.info("Creando emparejamientos temporales de análisis")
120
+
121
+ # Convertir timestamps a objetos datetime para comparación
122
+ situation_times = []
123
+ for analysis in situation_analyses:
124
+ if 'timestamp' in analysis:
125
+ try:
126
+ timestamp_str = analysis['timestamp']
127
+ dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
128
+ situation_times.append((dt, analysis))
129
+ except Exception as e:
130
+ logger.error(f"Error parseando timestamp de situación: {str(e)}")
131
+
132
+ recommendation_times = []
133
+ for recommendation in claude_recommendations:
134
+ if 'timestamp' in recommendation:
135
+ try:
136
+ timestamp_str = recommendation['timestamp']
137
+ dt = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
138
+ recommendation_times.append((dt, recommendation))
139
+ except Exception as e:
140
+ logger.error(f"Error parseando timestamp de recomendación: {str(e)}")
141
+
142
+ # Ordenar por tiempo
143
+ situation_times.sort(key=lambda x: x[0], reverse=True)
144
+ recommendation_times.sort(key=lambda x: x[0], reverse=True)
145
+
146
+ # Crear pares combinados
147
+ combined_items = []
148
+
149
+ # Primero, procesar todas las situaciones encontrando la recomendación más cercana
150
+ for sit_time, situation in situation_times:
151
+ # Buscar la recomendación más cercana en tiempo
152
+ best_match = None
153
+ min_diff = timedelta(minutes=30) # Máxima diferencia de tiempo aceptable (30 minutos)
154
+ best_rec_time = None
155
+
156
+ for rec_time, recommendation in recommendation_times:
157
+ time_diff = abs(sit_time - rec_time)
158
+ if time_diff < min_diff:
159
+ min_diff = time_diff
160
+ best_match = recommendation
161
+ best_rec_time = rec_time
162
+
163
+ # Crear un elemento combinado
164
+ if best_match:
165
+ timestamp_key = sit_time.isoformat()
166
+ combined_items.append((timestamp_key, {
167
+ 'situation': situation,
168
+ 'recommendation': best_match,
169
+ 'time_diff': min_diff.total_seconds()
170
+ }))
171
+ # Eliminar la recomendación usada para no reutilizarla
172
+ recommendation_times = [(t, r) for t, r in recommendation_times if t != best_rec_time]
173
+ logger.info(f"Emparejado: Diagnóstico {sit_time} con Recomendación {best_rec_time} (diferencia: {min_diff})")
174
+ else:
175
+ # Si no hay recomendación cercana, solo incluir la situación
176
+ timestamp_key = sit_time.isoformat()
177
+ combined_items.append((timestamp_key, {
178
+ 'situation': situation
179
+ }))
180
+ logger.info(f"Sin emparejar: Diagnóstico {sit_time} sin recomendación cercana")
181
+
182
+ # Agregar recomendaciones restantes sin situación
183
+ for rec_time, recommendation in recommendation_times:
184
+ timestamp_key = rec_time.isoformat()
185
+ combined_items.append((timestamp_key, {
186
+ 'recommendation': recommendation
187
+ }))
188
+ logger.info(f"Sin emparejar: Recomendación {rec_time} sin diagnóstico cercano")
189
+
190
+ # Ordenar por tiempo (más reciente primero)
191
+ combined_items.sort(key=lambda x: x[0], reverse=True)
192
+
193
+ logger.info(f"Procesando {len(combined_items)} elementos combinados")
194
+
195
+ # Mostrar cada par combinado
196
+ for i, (timestamp_key, analysis_pair) in enumerate(combined_items):
197
+ try:
198
+ # Obtener datos de situación y recomendación
199
+ situation_data = analysis_pair.get('situation', {})
200
+ recommendation_data = analysis_pair.get('recommendation', {})
201
+ time_diff = analysis_pair.get('time_diff')
202
+
203
+ # Si no hay ningún dato, continuar al siguiente
204
+ if not situation_data and not recommendation_data:
205
+ continue
206
+
207
+ # Determinar qué texto mostrar (priorizar el de la situación)
208
+ text_to_show = situation_data.get('text', recommendation_data.get('text', ''))
209
+ text_type = situation_data.get('text_type', recommendation_data.get('text_type', ''))
210
+
211
+ # Formatear fecha para mostrar
212
+ try:
213
+ # Usar timestamp del key que ya es un formato ISO
214
+ dt = datetime.fromisoformat(timestamp_key)
215
+ formatted_date = dt.strftime("%d/%m/%Y %H:%M:%S")
216
+ except Exception as date_error:
217
+ logger.error(f"Error formateando fecha: {str(date_error)}")
218
+ formatted_date = timestamp_key
219
+
220
+ # Determinar el título del expander
221
+ title = f"{t.get('analysis_date', 'Fecha')}: {formatted_date}"
222
+ if text_type:
223
+ text_type_display = {
224
+ 'academic_article': t.get('academic_article', 'Artículo académico'),
225
+ 'student_essay': t.get('student_essay', 'Trabajo universitario'),
226
+ 'general_communication': t.get('general_communication', 'Comunicación general')
227
+ }.get(text_type, text_type)
228
+ title += f" - {text_type_display}"
229
+
230
+ # Añadir indicador de emparejamiento si existe
231
+ if time_diff is not None:
232
+ if time_diff < 60: # menos de un minuto
233
+ title += f" 🔄 (emparejados)"
234
+ else:
235
+ title += f" 🔄 (emparejados, diferencia: {int(time_diff//60)} min)"
236
+
237
+ # Usar un ID único para cada expander
238
+ expander_id = f"analysis_{i}_{timestamp_key.replace(':', '_')}"
239
+
240
+ # Mostrar el análisis en un expander
241
+ with st.expander(title, expanded=False):
242
+ # Mostrar texto analizado con key único
243
+ st.subheader(t.get('analyzed_text', 'Texto analizado'))
244
+ st.text_area(
245
+ "Text Content",
246
+ value=text_to_show,
247
+ height=100,
248
+ disabled=True,
249
+ label_visibility="collapsed",
250
+ key=f"text_area_{expander_id}"
251
+ )
252
+
253
+ # Crear tabs para separar diagnóstico y recomendaciones
254
+ diagnosis_tab, recommendations_tab = st.tabs([
255
+ t.get('diagnosis_tab', 'Diagnóstico'),
256
+ t.get('recommendations_tab', 'Recomendaciones')
257
+ ])
258
+
259
+ # Tab de diagnóstico
260
+ with diagnosis_tab:
261
+ if situation_data and 'metrics' in situation_data:
262
+ metrics = situation_data['metrics']
263
+
264
+ # Dividir en dos columnas
265
+ col1, col2 = st.columns(2)
266
+
267
+ # Principales métricas en formato de tarjetas
268
+ with col1:
269
+ st.subheader(t.get('key_metrics', 'Métricas clave'))
270
+
271
+ # Mostrar cada métrica principal
272
+ for metric_name, metric_data in metrics.items():
273
+ try:
274
+ # Determinar la puntuación
275
+ score = None
276
+ if isinstance(metric_data, dict):
277
+ # Intentar diferentes nombres de campo
278
+ if 'normalized_score' in metric_data:
279
+ score = metric_data['normalized_score']
280
+ elif 'score' in metric_data:
281
+ score = metric_data['score']
282
+ elif 'value' in metric_data:
283
+ score = metric_data['value']
284
+ elif isinstance(metric_data, (int, float)):
285
+ score = metric_data
286
+
287
+ if score is not None:
288
+ # Asegurarse de que score es numérico
289
+ if isinstance(score, (int, float)):
290
+ # Determinar color y emoji basado en la puntuación
291
+ if score < 0.5:
292
+ emoji = "🔴"
293
+ color = "#ffcccc" # light red
294
+ elif score < 0.75:
295
+ emoji = "🟡"
296
+ color = "#ffffcc" # light yellow
297
+ else:
298
+ emoji = "🟢"
299
+ color = "#ccffcc" # light green
300
+
301
+ # Mostrar la métrica con estilo
302
+ st.markdown(f"""
303
+ <div style="background-color:{color}; padding:10px; border-radius:5px; margin-bottom:10px;">
304
+ <b>{emoji} {metric_name.capitalize()}:</b> {score:.2f}
305
+ </div>
306
+ """, unsafe_allow_html=True)
307
+ else:
308
+ # Si no es numérico, mostrar como texto
309
+ st.markdown(f"""
310
+ <div style="background-color:#f0f0f0; padding:10px; border-radius:5px; margin-bottom:10px;">
311
+ <b>ℹ️ {metric_name.capitalize()}:</b> {str(score)}
312
+ </div>
313
+ """, unsafe_allow_html=True)
314
+ except Exception as e:
315
+ logger.error(f"Error procesando métrica {metric_name}: {str(e)}")
316
+
317
+ # Mostrar detalles adicionales si están disponibles
318
+ with col2:
319
+ st.subheader(t.get('details', 'Detalles'))
320
+
321
+ # Para cada métrica, mostrar sus detalles si existen
322
+ for metric_name, metric_data in metrics.items():
323
+ try:
324
+ if isinstance(metric_data, dict):
325
+ # Mostrar detalles directamente o buscar en subcampos
326
+ details = None
327
+ if 'details' in metric_data and metric_data['details']:
328
+ details = metric_data['details']
329
+ else:
330
+ # Crear un diccionario con los detalles excluyendo 'normalized_score' y similares
331
+ details = {k: v for k, v in metric_data.items()
332
+ if k not in ['normalized_score', 'score', 'value']}
333
+
334
+ if details:
335
+ st.write(f"**{metric_name.capitalize()}**")
336
+ st.json(details, expanded=False)
337
+ except Exception as e:
338
+ logger.error(f"Error mostrando detalles de {metric_name}: {str(e)}")
339
+ else:
340
+ st.info(t.get('no_diagnosis', 'No hay datos de diagnóstico disponibles'))
341
+
342
+ # Tab de recomendaciones
343
+ with recommendations_tab:
344
+ if recommendation_data and 'recommendations' in recommendation_data:
345
+ st.markdown(f"""
346
+ <div style="padding: 20px; border-radius: 10px;
347
+ background-color: #f8f9fa; margin-bottom: 20px;">
348
+ {recommendation_data['recommendations']}
349
+ </div>
350
+ """, unsafe_allow_html=True)
351
+ elif recommendation_data and 'feedback' in recommendation_data:
352
+ st.markdown(f"""
353
+ <div style="padding: 20px; border-radius: 10px;
354
+ background-color: #f8f9fa; margin-bottom: 20px;">
355
+ {recommendation_data['feedback']}
356
+ </div>
357
+ """, unsafe_allow_html=True)
358
+ else:
359
+ st.info(t.get('no_recommendations', 'No hay recomendaciones disponibles'))
360
+
361
+ except Exception as e:
362
+ logger.error(f"Error procesando par de análisis: {str(e)}")
363
+ continue
364
+
365
+ except Exception as e:
366
+ logger.error(f"Error mostrando actividades de situación actual: {str(e)}")
367
+ st.error(t.get('error_current_situation', 'Error al mostrar análisis de situación actual'))
368
+
369
+ ###############################################################################################
370
+
371
+ def display_morphosyntax_activities(username: str, t: dict):
372
+ """Muestra actividades de análisis morfosintáctico"""
373
+ try:
374
+ analyses = get_student_morphosyntax_analysis(username)
375
+ if not analyses:
376
+ st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
377
+ return
378
+
379
+ for analysis in analyses:
380
+ with st.expander(
381
+ f"{t.get('analysis_date', 'Fecha')}: {analysis['timestamp']}",
382
+ expanded=False
383
+ ):
384
+ st.text(f"{t.get('analyzed_text', 'Texto analizado')}:")
385
+ st.write(analysis['text'])
386
+
387
+ if 'arc_diagrams' in analysis:
388
+ st.subheader(t.get('syntactic_diagrams', 'Diagramas sintácticos'))
389
+ for diagram in analysis['arc_diagrams']:
390
+ st.write(diagram, unsafe_allow_html=True)
391
+
392
+ except Exception as e:
393
+ logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
394
+ st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
395
+
396
+
397
+ ###############################################################################################
398
+
399
+ def display_semantic_activities(username: str, t: dict):
400
+ """Muestra actividades de análisis semántico"""
401
+ try:
402
+ logger.info(f"Recuperando análisis semántico para {username}")
403
+ analyses = get_student_semantic_analysis(username)
404
+
405
+ if not analyses:
406
+ logger.info("No se encontraron análisis semánticos")
407
+ st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
408
+ return
409
+
410
+ logger.info(f"Procesando {len(analyses)} análisis semánticos")
411
+
412
+ for analysis in analyses:
413
+ try:
414
+ # Verificar campos necesarios
415
+ if not all(key in analysis for key in ['timestamp', 'concept_graph']):
416
+ logger.warning(f"Análisis incompleto: {analysis.keys()}")
417
+ continue
418
+
419
+ # Formatear fecha
420
+ timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
421
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
422
+
423
+ # Crear expander
424
+ with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
425
+ # Procesar y mostrar gráfico
426
+ if analysis.get('concept_graph'):
427
+ try:
428
+ # Convertir de base64 a bytes
429
+ logger.debug("Decodificando gráfico de conceptos")
430
+ image_data = analysis['concept_graph']
431
+
432
+ # Si el gráfico ya es bytes, usarlo directamente
433
+ if isinstance(image_data, bytes):
434
+ image_bytes = image_data
435
+ else:
436
+ # Si es string base64, decodificar
437
+ image_bytes = base64.b64decode(image_data)
438
+
439
+ logger.debug(f"Longitud de bytes de imagen: {len(image_bytes)}")
440
+
441
+ # Mostrar imagen
442
+ st.image(
443
+ image_bytes,
444
+ caption=t.get('concept_network', 'Red de Conceptos'),
445
+ use_container_width=True
446
+ )
447
+ logger.debug("Gráfico mostrado exitosamente")
448
+
449
+ except Exception as img_error:
450
+ logger.error(f"Error procesando gráfico: {str(img_error)}")
451
+ st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
452
+ else:
453
+ st.info(t.get('no_graph', 'No hay visualización disponible'))
454
+
455
+ except Exception as e:
456
+ logger.error(f"Error procesando análisis individual: {str(e)}")
457
+ continue
458
+
459
+ except Exception as e:
460
+ logger.error(f"Error mostrando análisis semántico: {str(e)}")
461
+ st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
462
+
463
+
464
+ ###################################################################################################
465
+ def display_discourse_activities(username: str, t: dict):
466
+ """Muestra actividades de análisis del discurso (mostrado como 'Análisis comparado de textos' en la UI)"""
467
+ try:
468
+ logger.info(f"Recuperando análisis del discurso para {username}")
469
+ analyses = get_student_discourse_analysis(username)
470
+
471
+ if not analyses:
472
+ logger.info("No se encontraron análisis del discurso")
473
+ # Usamos el término "análisis comparado de textos" en la UI
474
+ st.info(t.get('no_discourse_analyses', 'No hay análisis comparados de textos registrados'))
475
+ return
476
+
477
+ logger.info(f"Procesando {len(analyses)} análisis del discurso")
478
+ for analysis in analyses:
479
+ try:
480
+ # Verificar campos mínimos necesarios
481
+ if not all(key in analysis for key in ['timestamp', 'combined_graph']):
482
+ logger.warning(f"Análisis incompleto: {analysis.keys()}")
483
+ continue
484
+
485
+ # Formatear fecha
486
+ timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
487
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
488
+
489
+ with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
490
+ if analysis['combined_graph']:
491
+ logger.debug("Decodificando gráfico combinado")
492
+ try:
493
+ image_bytes = base64.b64decode(analysis['combined_graph'])
494
+ st.image(image_bytes, use_column_width=True)
495
+ logger.debug("Gráfico mostrado exitosamente")
496
+ except Exception as img_error:
497
+ logger.error(f"Error decodificando imagen: {str(img_error)}")
498
+ st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
499
+ else:
500
+ st.info(t.get('no_visualization', 'No hay visualización comparativa disponible'))
501
+
502
+ except Exception as e:
503
+ logger.error(f"Error procesando análisis individual: {str(e)}")
504
+ continue
505
+
506
+ except Exception as e:
507
+ logger.error(f"Error mostrando análisis del discurso: {str(e)}")
508
+ # Usamos el término "análisis comparado de textos" en la UI
509
+ st.error(t.get('error_discourse', 'Error al mostrar análisis comparado de textos'))
510
+
511
+ #################################################################################
512
+ def display_chat_activities(username: str, t: dict):
513
+ """
514
+ Muestra historial de conversaciones del chat
515
+ """
516
+ try:
517
+ # Obtener historial del chat
518
+ chat_history = get_chat_history(
519
+ username=username,
520
+ analysis_type='sidebar',
521
+ limit=50
522
+ )
523
+
524
+ if not chat_history:
525
+ st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
526
+ return
527
+
528
+ for chat in reversed(chat_history): # Mostrar las más recientes primero
529
+ try:
530
+ # Convertir timestamp a datetime para formato
531
+ timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
532
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
533
+
534
+ with st.expander(
535
+ f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
536
+ expanded=False
537
+ ):
538
+ if 'messages' in chat and chat['messages']:
539
+ # Mostrar cada mensaje en la conversación
540
+ for message in chat['messages']:
541
+ role = message.get('role', 'unknown')
542
+ content = message.get('content', '')
543
+
544
+ # Usar el componente de chat de Streamlit
545
+ with st.chat_message(role):
546
+ st.markdown(content)
547
+
548
+ # Agregar separador entre mensajes
549
+ st.divider()
550
+ else:
551
+ st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
552
+
553
+ except Exception as e:
554
+ logger.error(f"Error mostrando conversación: {str(e)}")
555
+ continue
556
+
557
+ except Exception as e:
558
+ logger.error(f"Error mostrando historial del chat: {str(e)}")
559
+ st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
560
+
561
+ #################################################################################
562
+ def display_discourse_comparison(analysis: dict, t: dict):
563
+ """Muestra la comparación de análisis del discurso"""
564
+ # Cambiado para usar "textos comparados" en la UI
565
+ st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
566
+
567
+ col1, col2 = st.columns(2)
568
+ with col1:
569
+ st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}**")
570
+ df1 = pd.DataFrame(analysis['key_concepts1'])
571
+ st.dataframe(df1)
572
+
573
+ with col2:
574
+ st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}**")
575
+ df2 = pd.DataFrame(analysis['key_concepts2'])
576
  st.dataframe(df2)