AIdeaText commited on
Commit
1a269b6
·
verified ·
1 Parent(s): a79f910

Update modules/studentact/student_activities_v2.py

Browse files
modules/studentact/student_activities_v2.py CHANGED
@@ -1,780 +1,780 @@
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
- """
373
- Muestra actividades de análisis morfosintáctico, incluyendo base e iteraciones
374
- desde las nuevas colecciones: student_morphosyntax_analysis_base y student_morphosyntax_iterations
375
- """
376
- try:
377
- # Importación inline para evitar problemas de circularidad
378
- # Utilizamos la función de la nueva estructura de DB iterativa
379
- from ..database.morphosyntax_iterative_mongo_db import get_student_morphosyntax_analysis
380
-
381
- logger.info(f"Recuperando análisis morfosintáctico para {username}")
382
-
383
- # Esta función ahora trae tanto las bases como sus iteraciones
384
- base_analyses = get_student_morphosyntax_analysis(username)
385
-
386
- if not base_analyses:
387
- logger.info("No se encontraron análisis morfosintácticos")
388
- st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
389
- return
390
-
391
- logger.info(f"Procesando {len(base_analyses)} análisis morfosintácticos base")
392
-
393
- # Procesar cada análisis base con sus iteraciones
394
- for base_analysis in base_analyses:
395
- try:
396
- # Formatear fecha
397
- timestamp = datetime.fromisoformat(base_analysis['timestamp'].replace('Z', '+00:00'))
398
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
399
-
400
- # Título del expander: incluir información de si tiene iteraciones
401
- expander_title = f"{t.get('analysis_date', 'Fecha')}: {formatted_date}"
402
- if base_analysis.get('has_iterations', False):
403
- expander_title += f" ({t.get('has_iterations', 'Con iteraciones')})"
404
-
405
- with st.expander(expander_title, expanded=False):
406
- # Mostrar texto base
407
- st.subheader(t.get('base_text', 'Texto original'))
408
- st.text_area(
409
- "Base Text Content",
410
- value=base_analysis.get('text', ''),
411
- height=100,
412
- disabled=True,
413
- label_visibility="collapsed",
414
- key=f"base_text_{str(base_analysis['_id'])}"
415
- )
416
-
417
- # Mostrar diagrama de arco base si existe
418
- if 'arc_diagrams' in base_analysis and base_analysis['arc_diagrams']:
419
- st.subheader(t.get('syntactic_diagrams', 'Diagrama sintáctico (original)'))
420
- # Mostrar cada diagrama (normalmente solo uno por oración)
421
- for diagram in base_analysis['arc_diagrams']:
422
- st.write(diagram, unsafe_allow_html=True)
423
-
424
- # Procesar iteraciones si existen
425
- if 'iterations' in base_analysis and base_analysis['iterations']:
426
- st.markdown("---") # Línea divisoria
427
- st.subheader(t.get('iterations', 'Versiones mejoradas'))
428
-
429
- # Crear tabs para cada iteración
430
- iteration_tabs = st.tabs([
431
- f"{t.get('iteration', 'Versión')} {i+1}"
432
- for i in range(len(base_analysis['iterations']))
433
- ])
434
-
435
- # Mostrar cada iteración en su propia pestaña
436
- for i, (tab, iteration) in enumerate(zip(iteration_tabs, base_analysis['iterations'])):
437
- with tab:
438
- # Timestamp de la iteración
439
- iter_timestamp = datetime.fromisoformat(
440
- iteration['timestamp'].replace('Z', '+00:00'))
441
- iter_formatted_date = iter_timestamp.strftime("%d/%m/%Y %H:%M:%S")
442
- st.caption(f"{t.get('iteration_date', 'Fecha de versión')}: {iter_formatted_date}")
443
-
444
- # Texto de la iteración
445
- st.text_area(
446
- f"Iteration Text {i+1}",
447
- value=iteration.get('iteration_text', ''),
448
- height=100,
449
- disabled=True,
450
- label_visibility="collapsed",
451
- key=f"iter_text_{str(iteration['_id'])}"
452
- )
453
-
454
- # Diagrama de arco de la iteración
455
- if 'arc_diagrams' in iteration and iteration['arc_diagrams']:
456
- st.subheader(t.get('iteration_diagram', 'Diagrama sintáctico (mejorado)'))
457
- for diagram in iteration['arc_diagrams']:
458
- st.write(diagram, unsafe_allow_html=True)
459
-
460
- except Exception as e:
461
- logger.error(f"Error procesando análisis morfosintáctico: {str(e)}")
462
- st.error(t.get('error_processing_analysis', 'Error procesando este análisis'))
463
- continue
464
-
465
- except Exception as e:
466
- logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
467
- st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
468
-
469
-
470
- ###############################################################################################
471
-
472
- def display_semantic_activities(username: str, t: dict):
473
- """Muestra actividades de análisis semántico"""
474
- try:
475
- logger.info(f"Recuperando análisis semántico para {username}")
476
- analyses = get_student_semantic_analysis(username)
477
-
478
- if not analyses:
479
- logger.info("No se encontraron análisis semánticos")
480
- st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
481
- return
482
-
483
- logger.info(f"Procesando {len(analyses)} análisis semánticos")
484
-
485
- for analysis in analyses:
486
- try:
487
- # Verificar campos necesarios
488
- if not all(key in analysis for key in ['timestamp', 'concept_graph']):
489
- logger.warning(f"Análisis incompleto: {analysis.keys()}")
490
- continue
491
-
492
- # Formatear fecha
493
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
494
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
495
-
496
- # Crear expander
497
- with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
498
- # Procesar y mostrar gráfico
499
- if analysis.get('concept_graph'):
500
- try:
501
- # Convertir de base64 a bytes
502
- logger.debug("Decodificando gráfico de conceptos")
503
- image_data = analysis['concept_graph']
504
-
505
- # Si el gráfico ya es bytes, usarlo directamente
506
- if isinstance(image_data, bytes):
507
- image_bytes = image_data
508
- else:
509
- # Si es string base64, decodificar
510
- image_bytes = base64.b64decode(image_data)
511
-
512
- logger.debug(f"Longitud de bytes de imagen: {len(image_bytes)}")
513
-
514
- # Mostrar imagen
515
- st.image(
516
- image_bytes,
517
- caption=t.get('concept_network', 'Red de Conceptos'),
518
- use_container_width=True
519
- )
520
- logger.debug("Gráfico mostrado exitosamente")
521
-
522
- except Exception as img_error:
523
- logger.error(f"Error procesando gráfico: {str(img_error)}")
524
- st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
525
- else:
526
- st.info(t.get('no_graph', 'No hay visualización disponible'))
527
-
528
- except Exception as e:
529
- logger.error(f"Error procesando análisis individual: {str(e)}")
530
- continue
531
-
532
- except Exception as e:
533
- logger.error(f"Error mostrando análisis semántico: {str(e)}")
534
- st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
535
-
536
-
537
- ###################################################################################################
538
-
539
- def display_discourse_activities(username: str, t: dict):
540
- """Muestra actividades de análisis del discurso (mostrado como 'Análisis comparado de textos' en la UI)"""
541
- try:
542
- logger.info(f"Recuperando análisis del discurso para {username}")
543
- analyses = get_student_discourse_analysis(username)
544
-
545
- if not analyses:
546
- logger.info("No se encontraron análisis del discurso")
547
- # Usamos el término "análisis comparado de textos" en la UI
548
- st.info(t.get('no_discourse_analyses', 'No hay análisis comparados de textos registrados'))
549
- return
550
-
551
- logger.info(f"Procesando {len(analyses)} análisis del discurso")
552
- for analysis in analyses:
553
- try:
554
- # Verificar campos mínimos necesarios
555
- if not all(key in analysis for key in ['timestamp']):
556
- logger.warning(f"Análisis incompleto: {analysis.keys()}")
557
- continue
558
-
559
- # Formatear fecha
560
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
561
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
562
-
563
- with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
564
- # Crear dos columnas para mostrar los documentos lado a lado
565
- col1, col2 = st.columns(2)
566
-
567
- # Documento 1 - Columna izquierda
568
- with col1:
569
- st.subheader(t.get('doc1_title', 'Documento 1'))
570
- st.markdown(t.get('key_concepts', 'Conceptos Clave'))
571
-
572
- # Mostrar conceptos clave en formato de etiquetas
573
- if 'key_concepts1' in analysis and analysis['key_concepts1']:
574
- concepts_html = f"""
575
- <div style="display: flex; flex-wrap: nowrap; gap: 8px; padding: 12px;
576
- background-color: #f8f9fa; border-radius: 8px; overflow-x: auto;
577
- margin-bottom: 15px; white-space: nowrap;">
578
- {''.join([
579
- f'<div style="background-color: white; border-radius: 4px; padding: 6px 10px; display: inline-flex; align-items: center; gap: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); flex-shrink: 0;">'
580
- f'<span style="font-weight: 500; color: #1f2937; font-size: 0.85em;">{concept}</span>'
581
- f'<span style="color: #6b7280; font-size: 0.75em;">({freq:.2f})</span></div>'
582
- for concept, freq in analysis['key_concepts1']
583
- ])}
584
- </div>
585
- """
586
- st.markdown(concepts_html, unsafe_allow_html=True)
587
- else:
588
- st.info(t.get('no_concepts', 'No hay conceptos disponibles'))
589
-
590
- # Mostrar grafo 1
591
- if 'graph1' in analysis:
592
- try:
593
- if isinstance(analysis['graph1'], bytes):
594
- st.image(
595
- analysis['graph1'],
596
- use_container_width=True
597
- )
598
- else:
599
- logger.warning(f"graph1 no es bytes: {type(analysis['graph1'])}")
600
- st.warning(t.get('graph_not_available', 'Gráfico no disponible'))
601
- except Exception as e:
602
- logger.error(f"Error mostrando graph1: {str(e)}")
603
- st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
604
- else:
605
- st.info(t.get('no_visualization', 'No hay visualización disponible'))
606
-
607
- # Interpretación del grafo
608
- st.markdown("**📊 Interpretación del grafo:**")
609
- st.markdown("""
610
- - 🔀 Las flechas indican la dirección de la relación entre conceptos
611
- - 🎨 Los colores más intensos indican conceptos más centrales en el texto
612
- - ⭕ El tamaño de los nodos representa la frecuencia del concepto
613
- - ↔️ El grosor de las líneas indica la fuerza de la conexión
614
- """)
615
-
616
- # Documento 2 - Columna derecha
617
- with col2:
618
- st.subheader(t.get('doc2_title', 'Documento 2'))
619
- st.markdown(t.get('key_concepts', 'Conceptos Clave'))
620
-
621
- # Mostrar conceptos clave en formato de etiquetas
622
- if 'key_concepts2' in analysis and analysis['key_concepts2']:
623
- concepts_html = f"""
624
- <div style="display: flex; flex-wrap: nowrap; gap: 8px; padding: 12px;
625
- background-color: #f8f9fa; border-radius: 8px; overflow-x: auto;
626
- margin-bottom: 15px; white-space: nowrap;">
627
- {''.join([
628
- f'<div style="background-color: white; border-radius: 4px; padding: 6px 10px; display: inline-flex; align-items: center; gap: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); flex-shrink: 0;">'
629
- f'<span style="font-weight: 500; color: #1f2937; font-size: 0.85em;">{concept}</span>'
630
- f'<span style="color: #6b7280; font-size: 0.75em;">({freq:.2f})</span></div>'
631
- for concept, freq in analysis['key_concepts2']
632
- ])}
633
- </div>
634
- """
635
- st.markdown(concepts_html, unsafe_allow_html=True)
636
- else:
637
- st.info(t.get('no_concepts', 'No hay conceptos disponibles'))
638
-
639
- # Mostrar grafo 2
640
- if 'graph2' in analysis:
641
- try:
642
- if isinstance(analysis['graph2'], bytes):
643
- st.image(
644
- analysis['graph2'],
645
- use_container_width=True
646
- )
647
- else:
648
- logger.warning(f"graph2 no es bytes: {type(analysis['graph2'])}")
649
- st.warning(t.get('graph_not_available', 'Gráfico no disponible'))
650
- except Exception as e:
651
- logger.error(f"Error mostrando graph2: {str(e)}")
652
- st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
653
- else:
654
- st.info(t.get('no_visualization', 'No hay visualización disponible'))
655
-
656
- # Interpretación del grafo
657
- st.markdown("**📊 Interpretación del grafo:**")
658
- st.markdown("""
659
- - 🔀 Las flechas indican la dirección de la relación entre conceptos
660
- - 🎨 Los colores más intensos indican conceptos más centrales en el texto
661
- - ⭕ El tamaño de los nodos representa la frecuencia del concepto
662
- - ↔️ El grosor de las líneas indica la fuerza de la conexión
663
- """)
664
-
665
- except Exception as e:
666
- logger.error(f"Error procesando análisis individual: {str(e)}")
667
- continue
668
-
669
- except Exception as e:
670
- logger.error(f"Error mostrando análisis del discurso: {str(e)}")
671
- # Usamos el término "análisis comparado de textos" en la UI
672
- st.error(t.get('error_discourse', 'Error al mostrar análisis comparado de textos'))
673
-
674
-
675
-
676
- #################################################################################
677
-
678
- def display_discourse_comparison(analysis: dict, t: dict):
679
- """
680
- Muestra la comparación de conceptos clave en análisis del discurso.
681
- Formato horizontal simplificado.
682
- """
683
- st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
684
-
685
- # Verificar si tenemos los conceptos necesarios
686
- if not ('key_concepts1' in analysis and analysis['key_concepts1']):
687
- st.info(t.get('no_concepts', 'No hay conceptos disponibles para comparar'))
688
- return
689
-
690
- # Conceptos del Texto 1 - Formato horizontal
691
- st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}:**")
692
- try:
693
- # Comprobar formato y mostrar horizontalmente
694
- if isinstance(analysis['key_concepts1'], list) and len(analysis['key_concepts1']) > 0:
695
- if isinstance(analysis['key_concepts1'][0], list) and len(analysis['key_concepts1'][0]) == 2:
696
- # Formatear como "concepto (valor), concepto2 (valor2), ..."
697
- concepts_text = ", ".join([f"{c[0]} ({c[1]})" for c in analysis['key_concepts1'][:10]])
698
- st.markdown(f"*{concepts_text}*")
699
- else:
700
- # Si no tiene el formato esperado, mostrar como lista simple
701
- st.markdown(", ".join(str(c) for c in analysis['key_concepts1'][:10]))
702
- else:
703
- st.write(str(analysis['key_concepts1']))
704
- except Exception as e:
705
- logger.error(f"Error mostrando key_concepts1: {str(e)}")
706
- st.error(t.get('error_concepts1', 'Error mostrando conceptos del Texto 1'))
707
-
708
- # Conceptos del Texto 2 - Formato horizontal
709
- st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}:**")
710
- if 'key_concepts2' in analysis and analysis['key_concepts2']:
711
- try:
712
- # Comprobar formato y mostrar horizontalmente
713
- if isinstance(analysis['key_concepts2'], list) and len(analysis['key_concepts2']) > 0:
714
- if isinstance(analysis['key_concepts2'][0], list) and len(analysis['key_concepts2'][0]) == 2:
715
- # Formatear como "concepto (valor), concepto2 (valor2), ..."
716
- concepts_text = ", ".join([f"{c[0]} ({c[1]})" for c in analysis['key_concepts2'][:10]])
717
- st.markdown(f"*{concepts_text}*")
718
- else:
719
- # Si no tiene el formato esperado, mostrar como lista simple
720
- st.markdown(", ".join(str(c) for c in analysis['key_concepts2'][:10]))
721
- else:
722
- st.write(str(analysis['key_concepts2']))
723
- except Exception as e:
724
- logger.error(f"Error mostrando key_concepts2: {str(e)}")
725
- st.error(t.get('error_concepts2', 'Error mostrando conceptos del Texto 2'))
726
- else:
727
- st.info(t.get('no_concepts2', 'No hay conceptos disponibles para el Texto 2'))
728
-
729
-
730
- #################################################################################
731
- def display_chat_activities(username: str, t: dict):
732
- """
733
- Muestra historial de conversaciones del chat
734
- """
735
- try:
736
- # Obtener historial del chat
737
- chat_history = get_chat_history(
738
- username=username,
739
- analysis_type='sidebar',
740
- limit=50
741
- )
742
-
743
- if not chat_history:
744
- st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
745
- return
746
-
747
- for chat in reversed(chat_history): # Mostrar las más recientes primero
748
- try:
749
- # Convertir timestamp a datetime para formato
750
- timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
751
- formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
752
-
753
- with st.expander(
754
- f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
755
- expanded=False
756
- ):
757
- if 'messages' in chat and chat['messages']:
758
- # Mostrar cada mensaje en la conversación
759
- for message in chat['messages']:
760
- role = message.get('role', 'unknown')
761
- content = message.get('content', '')
762
-
763
- # Usar el componente de chat de Streamlit
764
- with st.chat_message(role):
765
- st.markdown(content)
766
-
767
- # Agregar separador entre mensajes
768
- st.divider()
769
- else:
770
- st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
771
-
772
- except Exception as e:
773
- logger.error(f"Error mostrando conversación: {str(e)}")
774
- continue
775
-
776
- except Exception as e:
777
- logger.error(f"Error mostrando historial del chat: {str(e)}")
778
- st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
779
-
780
- #################################################################################
 
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[0]:
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[1]:
70
+ display_discourse_activities(username, t)
71
+
72
+ # Tab de Conversaciones del Chat
73
+ with tabs[2]:
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
+ """
373
+ Muestra actividades de análisis morfosintáctico, incluyendo base e iteraciones
374
+ desde las nuevas colecciones: student_morphosyntax_analysis_base y student_morphosyntax_iterations
375
+ """
376
+ try:
377
+ # Importación inline para evitar problemas de circularidad
378
+ # Utilizamos la función de la nueva estructura de DB iterativa
379
+ from ..database.morphosyntax_iterative_mongo_db import get_student_morphosyntax_analysis
380
+
381
+ logger.info(f"Recuperando análisis morfosintáctico para {username}")
382
+
383
+ # Esta función ahora trae tanto las bases como sus iteraciones
384
+ base_analyses = get_student_morphosyntax_analysis(username)
385
+
386
+ if not base_analyses:
387
+ logger.info("No se encontraron análisis morfosintácticos")
388
+ st.info(t.get('no_morpho_analyses', 'No hay análisis morfosintácticos registrados'))
389
+ return
390
+
391
+ logger.info(f"Procesando {len(base_analyses)} análisis morfosintácticos base")
392
+
393
+ # Procesar cada análisis base con sus iteraciones
394
+ for base_analysis in base_analyses:
395
+ try:
396
+ # Formatear fecha
397
+ timestamp = datetime.fromisoformat(base_analysis['timestamp'].replace('Z', '+00:00'))
398
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
399
+
400
+ # Título del expander: incluir información de si tiene iteraciones
401
+ expander_title = f"{t.get('analysis_date', 'Fecha')}: {formatted_date}"
402
+ if base_analysis.get('has_iterations', False):
403
+ expander_title += f" ({t.get('has_iterations', 'Con iteraciones')})"
404
+
405
+ with st.expander(expander_title, expanded=False):
406
+ # Mostrar texto base
407
+ st.subheader(t.get('base_text', 'Texto original'))
408
+ st.text_area(
409
+ "Base Text Content",
410
+ value=base_analysis.get('text', ''),
411
+ height=100,
412
+ disabled=True,
413
+ label_visibility="collapsed",
414
+ key=f"base_text_{str(base_analysis['_id'])}"
415
+ )
416
+
417
+ # Mostrar diagrama de arco base si existe
418
+ if 'arc_diagrams' in base_analysis and base_analysis['arc_diagrams']:
419
+ st.subheader(t.get('syntactic_diagrams', 'Diagrama sintáctico (original)'))
420
+ # Mostrar cada diagrama (normalmente solo uno por oración)
421
+ for diagram in base_analysis['arc_diagrams']:
422
+ st.write(diagram, unsafe_allow_html=True)
423
+
424
+ # Procesar iteraciones si existen
425
+ if 'iterations' in base_analysis and base_analysis['iterations']:
426
+ st.markdown("---") # Línea divisoria
427
+ st.subheader(t.get('iterations', 'Versiones mejoradas'))
428
+
429
+ # Crear tabs para cada iteración
430
+ iteration_tabs = st.tabs([
431
+ f"{t.get('iteration', 'Versión')} {i+1}"
432
+ for i in range(len(base_analysis['iterations']))
433
+ ])
434
+
435
+ # Mostrar cada iteración en su propia pestaña
436
+ for i, (tab, iteration) in enumerate(zip(iteration_tabs, base_analysis['iterations'])):
437
+ with tab:
438
+ # Timestamp de la iteración
439
+ iter_timestamp = datetime.fromisoformat(
440
+ iteration['timestamp'].replace('Z', '+00:00'))
441
+ iter_formatted_date = iter_timestamp.strftime("%d/%m/%Y %H:%M:%S")
442
+ st.caption(f"{t.get('iteration_date', 'Fecha de versión')}: {iter_formatted_date}")
443
+
444
+ # Texto de la iteración
445
+ st.text_area(
446
+ f"Iteration Text {i+1}",
447
+ value=iteration.get('iteration_text', ''),
448
+ height=100,
449
+ disabled=True,
450
+ label_visibility="collapsed",
451
+ key=f"iter_text_{str(iteration['_id'])}"
452
+ )
453
+
454
+ # Diagrama de arco de la iteración
455
+ if 'arc_diagrams' in iteration and iteration['arc_diagrams']:
456
+ st.subheader(t.get('iteration_diagram', 'Diagrama sintáctico (mejorado)'))
457
+ for diagram in iteration['arc_diagrams']:
458
+ st.write(diagram, unsafe_allow_html=True)
459
+
460
+ except Exception as e:
461
+ logger.error(f"Error procesando análisis morfosintáctico: {str(e)}")
462
+ st.error(t.get('error_processing_analysis', 'Error procesando este análisis'))
463
+ continue
464
+
465
+ except Exception as e:
466
+ logger.error(f"Error mostrando análisis morfosintáctico: {str(e)}")
467
+ st.error(t.get('error_morpho', 'Error al mostrar análisis morfosintáctico'))
468
+
469
+
470
+ ###############################################################################################
471
+
472
+ def display_semantic_activities(username: str, t: dict):
473
+ """Muestra actividades de análisis semántico"""
474
+ try:
475
+ logger.info(f"Recuperando análisis semántico para {username}")
476
+ analyses = get_student_semantic_analysis(username)
477
+
478
+ if not analyses:
479
+ logger.info("No se encontraron análisis semánticos")
480
+ st.info(t.get('no_semantic_analyses', 'No hay análisis semánticos registrados'))
481
+ return
482
+
483
+ logger.info(f"Procesando {len(analyses)} análisis semánticos")
484
+
485
+ for analysis in analyses:
486
+ try:
487
+ # Verificar campos necesarios
488
+ if not all(key in analysis for key in ['timestamp', 'concept_graph']):
489
+ logger.warning(f"Análisis incompleto: {analysis.keys()}")
490
+ continue
491
+
492
+ # Formatear fecha
493
+ timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
494
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
495
+
496
+ # Crear expander
497
+ with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
498
+ # Procesar y mostrar gráfico
499
+ if analysis.get('concept_graph'):
500
+ try:
501
+ # Convertir de base64 a bytes
502
+ logger.debug("Decodificando gráfico de conceptos")
503
+ image_data = analysis['concept_graph']
504
+
505
+ # Si el gráfico ya es bytes, usarlo directamente
506
+ if isinstance(image_data, bytes):
507
+ image_bytes = image_data
508
+ else:
509
+ # Si es string base64, decodificar
510
+ image_bytes = base64.b64decode(image_data)
511
+
512
+ logger.debug(f"Longitud de bytes de imagen: {len(image_bytes)}")
513
+
514
+ # Mostrar imagen
515
+ st.image(
516
+ image_bytes,
517
+ caption=t.get('concept_network', 'Red de Conceptos'),
518
+ use_container_width=True
519
+ )
520
+ logger.debug("Gráfico mostrado exitosamente")
521
+
522
+ except Exception as img_error:
523
+ logger.error(f"Error procesando gráfico: {str(img_error)}")
524
+ st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
525
+ else:
526
+ st.info(t.get('no_graph', 'No hay visualización disponible'))
527
+
528
+ except Exception as e:
529
+ logger.error(f"Error procesando análisis individual: {str(e)}")
530
+ continue
531
+
532
+ except Exception as e:
533
+ logger.error(f"Error mostrando análisis semántico: {str(e)}")
534
+ st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
535
+
536
+
537
+ ###################################################################################################
538
+
539
+ def display_discourse_activities(username: str, t: dict):
540
+ """Muestra actividades de análisis del discurso (mostrado como 'Análisis comparado de textos' en la UI)"""
541
+ try:
542
+ logger.info(f"Recuperando análisis del discurso para {username}")
543
+ analyses = get_student_discourse_analysis(username)
544
+
545
+ if not analyses:
546
+ logger.info("No se encontraron análisis del discurso")
547
+ # Usamos el término "análisis comparado de textos" en la UI
548
+ st.info(t.get('no_discourse_analyses', 'No hay análisis comparados de textos registrados'))
549
+ return
550
+
551
+ logger.info(f"Procesando {len(analyses)} análisis del discurso")
552
+ for analysis in analyses:
553
+ try:
554
+ # Verificar campos mínimos necesarios
555
+ if not all(key in analysis for key in ['timestamp']):
556
+ logger.warning(f"Análisis incompleto: {analysis.keys()}")
557
+ continue
558
+
559
+ # Formatear fecha
560
+ timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
561
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
562
+
563
+ with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
564
+ # Crear dos columnas para mostrar los documentos lado a lado
565
+ col1, col2 = st.columns(2)
566
+
567
+ # Documento 1 - Columna izquierda
568
+ with col1:
569
+ st.subheader(t.get('doc1_title', 'Documento 1'))
570
+ st.markdown(t.get('key_concepts', 'Conceptos Clave'))
571
+
572
+ # Mostrar conceptos clave en formato de etiquetas
573
+ if 'key_concepts1' in analysis and analysis['key_concepts1']:
574
+ concepts_html = f"""
575
+ <div style="display: flex; flex-wrap: nowrap; gap: 8px; padding: 12px;
576
+ background-color: #f8f9fa; border-radius: 8px; overflow-x: auto;
577
+ margin-bottom: 15px; white-space: nowrap;">
578
+ {''.join([
579
+ f'<div style="background-color: white; border-radius: 4px; padding: 6px 10px; display: inline-flex; align-items: center; gap: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); flex-shrink: 0;">'
580
+ f'<span style="font-weight: 500; color: #1f2937; font-size: 0.85em;">{concept}</span>'
581
+ f'<span style="color: #6b7280; font-size: 0.75em;">({freq:.2f})</span></div>'
582
+ for concept, freq in analysis['key_concepts1']
583
+ ])}
584
+ </div>
585
+ """
586
+ st.markdown(concepts_html, unsafe_allow_html=True)
587
+ else:
588
+ st.info(t.get('no_concepts', 'No hay conceptos disponibles'))
589
+
590
+ # Mostrar grafo 1
591
+ if 'graph1' in analysis:
592
+ try:
593
+ if isinstance(analysis['graph1'], bytes):
594
+ st.image(
595
+ analysis['graph1'],
596
+ use_container_width=True
597
+ )
598
+ else:
599
+ logger.warning(f"graph1 no es bytes: {type(analysis['graph1'])}")
600
+ st.warning(t.get('graph_not_available', 'Gráfico no disponible'))
601
+ except Exception as e:
602
+ logger.error(f"Error mostrando graph1: {str(e)}")
603
+ st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
604
+ else:
605
+ st.info(t.get('no_visualization', 'No hay visualización disponible'))
606
+
607
+ # Interpretación del grafo
608
+ st.markdown("**📊 Interpretación del grafo:**")
609
+ st.markdown("""
610
+ - 🔀 Las flechas indican la dirección de la relación entre conceptos
611
+ - 🎨 Los colores más intensos indican conceptos más centrales en el texto
612
+ - ⭕ El tamaño de los nodos representa la frecuencia del concepto
613
+ - ↔️ El grosor de las líneas indica la fuerza de la conexión
614
+ """)
615
+
616
+ # Documento 2 - Columna derecha
617
+ with col2:
618
+ st.subheader(t.get('doc2_title', 'Documento 2'))
619
+ st.markdown(t.get('key_concepts', 'Conceptos Clave'))
620
+
621
+ # Mostrar conceptos clave en formato de etiquetas
622
+ if 'key_concepts2' in analysis and analysis['key_concepts2']:
623
+ concepts_html = f"""
624
+ <div style="display: flex; flex-wrap: nowrap; gap: 8px; padding: 12px;
625
+ background-color: #f8f9fa; border-radius: 8px; overflow-x: auto;
626
+ margin-bottom: 15px; white-space: nowrap;">
627
+ {''.join([
628
+ f'<div style="background-color: white; border-radius: 4px; padding: 6px 10px; display: inline-flex; align-items: center; gap: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); flex-shrink: 0;">'
629
+ f'<span style="font-weight: 500; color: #1f2937; font-size: 0.85em;">{concept}</span>'
630
+ f'<span style="color: #6b7280; font-size: 0.75em;">({freq:.2f})</span></div>'
631
+ for concept, freq in analysis['key_concepts2']
632
+ ])}
633
+ </div>
634
+ """
635
+ st.markdown(concepts_html, unsafe_allow_html=True)
636
+ else:
637
+ st.info(t.get('no_concepts', 'No hay conceptos disponibles'))
638
+
639
+ # Mostrar grafo 2
640
+ if 'graph2' in analysis:
641
+ try:
642
+ if isinstance(analysis['graph2'], bytes):
643
+ st.image(
644
+ analysis['graph2'],
645
+ use_container_width=True
646
+ )
647
+ else:
648
+ logger.warning(f"graph2 no es bytes: {type(analysis['graph2'])}")
649
+ st.warning(t.get('graph_not_available', 'Gráfico no disponible'))
650
+ except Exception as e:
651
+ logger.error(f"Error mostrando graph2: {str(e)}")
652
+ st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
653
+ else:
654
+ st.info(t.get('no_visualization', 'No hay visualización disponible'))
655
+
656
+ # Interpretación del grafo
657
+ st.markdown("**📊 Interpretación del grafo:**")
658
+ st.markdown("""
659
+ - 🔀 Las flechas indican la dirección de la relación entre conceptos
660
+ - 🎨 Los colores más intensos indican conceptos más centrales en el texto
661
+ - ⭕ El tamaño de los nodos representa la frecuencia del concepto
662
+ - ↔️ El grosor de las líneas indica la fuerza de la conexión
663
+ """)
664
+
665
+ except Exception as e:
666
+ logger.error(f"Error procesando análisis individual: {str(e)}")
667
+ continue
668
+
669
+ except Exception as e:
670
+ logger.error(f"Error mostrando análisis del discurso: {str(e)}")
671
+ # Usamos el término "análisis comparado de textos" en la UI
672
+ st.error(t.get('error_discourse', 'Error al mostrar análisis comparado de textos'))
673
+
674
+
675
+
676
+ #################################################################################
677
+
678
+ def display_discourse_comparison(analysis: dict, t: dict):
679
+ """
680
+ Muestra la comparación de conceptos clave en análisis del discurso.
681
+ Formato horizontal simplificado.
682
+ """
683
+ st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
684
+
685
+ # Verificar si tenemos los conceptos necesarios
686
+ if not ('key_concepts1' in analysis and analysis['key_concepts1']):
687
+ st.info(t.get('no_concepts', 'No hay conceptos disponibles para comparar'))
688
+ return
689
+
690
+ # Conceptos del Texto 1 - Formato horizontal
691
+ st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}:**")
692
+ try:
693
+ # Comprobar formato y mostrar horizontalmente
694
+ if isinstance(analysis['key_concepts1'], list) and len(analysis['key_concepts1']) > 0:
695
+ if isinstance(analysis['key_concepts1'][0], list) and len(analysis['key_concepts1'][0]) == 2:
696
+ # Formatear como "concepto (valor), concepto2 (valor2), ..."
697
+ concepts_text = ", ".join([f"{c[0]} ({c[1]})" for c in analysis['key_concepts1'][:10]])
698
+ st.markdown(f"*{concepts_text}*")
699
+ else:
700
+ # Si no tiene el formato esperado, mostrar como lista simple
701
+ st.markdown(", ".join(str(c) for c in analysis['key_concepts1'][:10]))
702
+ else:
703
+ st.write(str(analysis['key_concepts1']))
704
+ except Exception as e:
705
+ logger.error(f"Error mostrando key_concepts1: {str(e)}")
706
+ st.error(t.get('error_concepts1', 'Error mostrando conceptos del Texto 1'))
707
+
708
+ # Conceptos del Texto 2 - Formato horizontal
709
+ st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}:**")
710
+ if 'key_concepts2' in analysis and analysis['key_concepts2']:
711
+ try:
712
+ # Comprobar formato y mostrar horizontalmente
713
+ if isinstance(analysis['key_concepts2'], list) and len(analysis['key_concepts2']) > 0:
714
+ if isinstance(analysis['key_concepts2'][0], list) and len(analysis['key_concepts2'][0]) == 2:
715
+ # Formatear como "concepto (valor), concepto2 (valor2), ..."
716
+ concepts_text = ", ".join([f"{c[0]} ({c[1]})" for c in analysis['key_concepts2'][:10]])
717
+ st.markdown(f"*{concepts_text}*")
718
+ else:
719
+ # Si no tiene el formato esperado, mostrar como lista simple
720
+ st.markdown(", ".join(str(c) for c in analysis['key_concepts2'][:10]))
721
+ else:
722
+ st.write(str(analysis['key_concepts2']))
723
+ except Exception as e:
724
+ logger.error(f"Error mostrando key_concepts2: {str(e)}")
725
+ st.error(t.get('error_concepts2', 'Error mostrando conceptos del Texto 2'))
726
+ else:
727
+ st.info(t.get('no_concepts2', 'No hay conceptos disponibles para el Texto 2'))
728
+
729
+
730
+ #################################################################################
731
+ def display_chat_activities(username: str, t: dict):
732
+ """
733
+ Muestra historial de conversaciones del chat
734
+ """
735
+ try:
736
+ # Obtener historial del chat
737
+ chat_history = get_chat_history(
738
+ username=username,
739
+ analysis_type='sidebar',
740
+ limit=50
741
+ )
742
+
743
+ if not chat_history:
744
+ st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
745
+ return
746
+
747
+ for chat in reversed(chat_history): # Mostrar las más recientes primero
748
+ try:
749
+ # Convertir timestamp a datetime para formato
750
+ timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
751
+ formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
752
+
753
+ with st.expander(
754
+ f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
755
+ expanded=False
756
+ ):
757
+ if 'messages' in chat and chat['messages']:
758
+ # Mostrar cada mensaje en la conversación
759
+ for message in chat['messages']:
760
+ role = message.get('role', 'unknown')
761
+ content = message.get('content', '')
762
+
763
+ # Usar el componente de chat de Streamlit
764
+ with st.chat_message(role):
765
+ st.markdown(content)
766
+
767
+ # Agregar separador entre mensajes
768
+ st.divider()
769
+ else:
770
+ st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
771
+
772
+ except Exception as e:
773
+ logger.error(f"Error mostrando conversación: {str(e)}")
774
+ continue
775
+
776
+ except Exception as e:
777
+ logger.error(f"Error mostrando historial del chat: {str(e)}")
778
+ st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
779
+
780
+ #################################################################################