AIdeaText commited on
Commit
dffc9b3
·
verified ·
1 Parent(s): 970f78f

Update modules/studentact/student_activities_v2.py

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